Testování bez produkčního databázového systému
Na této stránce probereme techniky pro psaní automatizovaných testů, které nezahrnují databázový systém, proti kterému aplikace běží v produkčním prostředí, a to tak, že prohodíte databázi testovat dvojitou. Existují různé typy dvojitých testů a přístupů k tomu a doporučuje se důkladně číst zvolit testovací strategii, plně porozumět různým možnostem. Nakonec je také možné testovat v produkčním databázovém systému; to je popsáno v Testování v produkčním databázovém systému.
Spropitné
Tato stránka ukazuje techniky xUnit, ale podobné koncepty existují i v jiných testovacích frameworkech, včetně NUnit.
Model úložiště
Pokud jste se rozhodli psát testy bez zapojení produkčního databázového systému, pak doporučený postup, jak to udělat, je vzor úložiště; Další informace o tomto tématu najdete v této části. Prvním krokem implementace vzoru úložiště je převedení dotazů EF Core v LINQ do samostatné vrstvy, kterou budeme později vzájemně propojit nebo simulovat. Tady je příklad rozhraní úložiště pro náš systém blogování:
public interface IBloggingRepository
{
Task<Blog> GetBlogByNameAsync(string name);
IAsyncEnumerable<Blog> GetAllBlogsAsync();
void AddBlog(Blog blog);
Task SaveChangesAsync();
}
... a tady je částečná ukázková implementace pro použití v produkčním prostředí:
public class BloggingRepository : IBloggingRepository
{
private readonly BloggingContext _context;
public BloggingRepository(BloggingContext context)
=> _context = context;
public async Task<Blog> GetBlogByNameAsync(string name)
=> await _context.Blogs.FirstOrDefaultAsync(b => b.Name == name);
// Other code...
}
Na tom není nic složitého: úložiště jednoduše zabalí kontext EF Core a poskytuje metody, které spouštějí databázové dotazy a aktualizace. Klíčovým bodem, který je třeba poznamenat, je, že naše metoda GetAllBlogs
vrací IAsyncEnumerable<Blog>
(nebo IEnumerable<Blog>
) a ne IQueryable<Blog>
. Vrácení druhého by znamenalo, že operátory dotazů by se stále daly skládat z výsledku, což by vyžadovalo, aby EF Core stále bylo zapojeno do překladu dotazu; to by popřelo smysl toho mít úložiště vůbec.
IAsyncEnumerable<Blog>
nám umožňuje snadno vytvářet stuby nebo mockovat to, co repository vrací.
Pro aplikaci ASP.NET Core potřebujeme zaregistrovat úložiště jako službu injektáž závislostí přidáním následujícího kódu do ConfigureServices
aplikace:
services.AddScoped<IBloggingRepository, BloggingRepository>();
Nakonec se naše kontrolery namísto kontextu EF Core injektují službou úložiště a používají na ní metody.
private readonly IBloggingRepository _repository;
public BloggingControllerWithRepository(IBloggingRepository repository)
=> _repository = repository;
[HttpGet]
public async Task<Blog> GetBlog(string name)
=> await _repository.GetBlogByNameAsync(name);
V tomto okamžiku je vaše aplikace navržená podle vzoru úložiště: jediný kontakt s vrstvou přístupu k datům – EF Core – je teď prostřednictvím vrstvy úložiště, která funguje jako mediátor mezi kódem aplikace a skutečnými databázovými dotazy. Testy se teď dají napsat jednoduše tak, že projdete úložištěm nebo si ho napodobíte s vaší oblíbenou knihovnou napodobování. Zde je příklad testu založeného na mocku pomocí oblíbené knihovny Moq:
[Fact]
public async Task GetBlog()
{
// Arrange
var repositoryMock = new Mock<IBloggingRepository>();
repositoryMock
.Setup(r => r.GetBlogByNameAsync("Blog2"))
.Returns(Task.FromResult(new Blog { Name = "Blog2", Url = "http://blog2.com" }));
var controller = new BloggingControllerWithRepository(repositoryMock.Object);
// Act
var blog = await controller.GetBlog("Blog2");
// Assert
repositoryMock.Verify(r => r.GetBlogByNameAsync("Blog2"));
Assert.Equal("http://blog2.com", blog.Url);
}
Úplný vzorový kód si můžete prohlédnout zde.
SQLite v paměti
SQLite lze snadno nakonfigurovat jako poskytovatele EF Core pro testovací sadu místo produkčního databázového systému (e.g. SQL Server); Podrobnosti najdete v dokumentaci poskytovatele SQLite. Při testování je ale obvykle vhodné použít funkci databáze V paměti databáze SQLite, protože poskytuje jednoduchou izolaci mezi testy a nevyžaduje práci se skutečnými soubory SQLite.
Pokud chcete používat SQLite v paměti, je důležité pochopit, že se nová databáze vytvoří při každém otevření připojení nízké úrovně a že se odstraní při zavření připojení. Při normálním využití EF Core DbContext
otevírá a podle potřeby zavírá připojení k databázi – při každém provedení dotazu – aby se připojení nezůstávala otevřená zbytečně dlouhou dobu. S využitím SQLite v paměti by to ale pokaždé vedlo k resetování databáze; takže jako alternativní řešení otevřeme připojení před jeho předáním do EF Core a zajistíme, aby se zavřela jenom v případě, že se test dokončí:
public SqliteInMemoryBloggingControllerTest()
{
// Create and open a connection. This creates the SQLite in-memory database, which will persist until the connection is closed
// at the end of the test (see Dispose below).
_connection = new SqliteConnection("Filename=:memory:");
_connection.Open();
// These options will be used by the context instances in this test suite, including the connection opened above.
_contextOptions = new DbContextOptionsBuilder<BloggingContext>()
.UseSqlite(_connection)
.Options;
// Create the schema and seed some data
using var context = new BloggingContext(_contextOptions);
if (context.Database.EnsureCreated())
{
using var viewCommand = context.Database.GetDbConnection().CreateCommand();
viewCommand.CommandText = @"
CREATE VIEW AllResources AS
SELECT Url
FROM Blogs;";
viewCommand.ExecuteNonQuery();
}
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
BloggingContext CreateContext() => new BloggingContext(_contextOptions);
public void Dispose() => _connection.Dispose();
Testy nyní mohou volat CreateContext
, což vrátí kontext pomocí připojení, které jsme nastavili v konstruktoru, abychom zajistili, že máme čistou databázi s počátečními daty.
Úplný vzorový kód pro testování SQLite v paměti lze zobrazit zde.
Poskytovatel v paměti
Jak je uvedeno na stránce s přehledem testování , použití poskytovatele v paměti pro testování se silně nedoporučuje. Zvažte místo toho použití SQLite, nebo implementaci vzoru úložiště. Pokud jste se rozhodli používat in-memory, tady je typický konstruktor testovací třídy, který nastaví a vytvoří novou databázi v paměti před každým testem:
public InMemoryBloggingControllerTest()
{
_contextOptions = new DbContextOptionsBuilder<BloggingContext>()
.UseInMemoryDatabase("BloggingControllerTest")
.ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.Options;
using var context = new BloggingContext(_contextOptions);
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
Úplný vzorový kód pro testování v paměti lze zobrazit zde.
Pojmenování databáze v paměti
Databáze v paměti jsou identifikovány jednoduchým názvem řetězce a je možné se několikrát připojit ke stejné databázi zadáním stejného názvu (proto výše uvedený vzorek musí volat EnsureDeleted
před každým testem). Mějte však na paměti, že databáze v paměti jsou kořenové v kontextu interního poskytovatele služeb; zatímco ve většině případů kontexty sdílejí stejného poskytovatele služeb, konfigurace kontextů s různými možnostmi může aktivovat použití nového interního poskytovatele služeb. V takovém případě explicitně předejte stejnou instanci InMemoryDatabaseRoot do UseInMemoryDatabase
pro všechny kontexty, které by měly sdílet databáze v paměti (obvykle se to provádí pomocí statického pole InMemoryDatabaseRoot
).
Transakce
Mějte na paměti, že ve výchozím nastavení, pokud je transakce spuštěna, poskytovatel pro ukládání v paměti vyvolá výjimku, protože transakce nejsou podporovány. Místo toho můžete chtít transakce bezobslužně ignorovat tím, že nakonfigurujete EF Core tak, aby ignorovaly InMemoryEventId.TransactionIgnoredWarning
jako v předchozí ukázce. Pokud ale váš kód skutečně spoléhá na transakční sémantiku – například závisí na vrácení změn zpět – váš test nebude fungovat.
Pohledy
Poskytovatel v paměti umožňuje definici zobrazení prostřednictvím dotazů LINQ pomocí ToInMemoryQuery:
modelBuilder.Entity<UrlResource>()
.ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));