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íme databázi s testovacím dvojitým testem. Existují různé typy dvojitých testů a přístupů k tomu a doporučuje se důkladně přečíst volbu testovací strategie , abyste plně porozuměli různým možnostem. Nakonec je také možné testovat v produkčním databázovém systému; Toto se vztahuje na testování v produkčním databázovém systému.

Tip

Tato stránka ukazuje techniky xUnit , ale podobné koncepty existují v jiných testovacích architekturách, včetně NUnit.

Vzor ú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 extrahování dotazů EF Core LINQ do samostatné vrstvy, kterou budeme později zasunout nebo napodobení. Tady je příklad rozhraní úložiště pro náš systém blogování:

public interface IBloggingRepository
{
    Blog GetBlogByName(string name);

    IEnumerable<Blog> GetAllBlogs();

    void AddBlog(Blog blog);

    void SaveChanges();
}

... 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 Blog GetBlogByName(string name)
        => _context.Blogs.FirstOrDefault(b => b.Name == name);

    // Other code...
}

Není toho moc: úložiště jednoduše zabalí kontext EF Core a zveřejňuje metody, které spouštějí databázové dotazy a aktualizace. Klíčovým bodem, který je třeba poznamenat, je, že naše GetAllBlogs metoda vrací IEnumerable<Blog>, a ne IQueryable<Blog>. Vrácení druhého by znamenalo, že operátory dotazů se stále dají skládat z výsledku, což vyžaduje, aby ef Core stále bylo zapojeno do překladu dotazu; to by poškodilo účel mít úložiště na prvním místě. IEnumerable<Blog> umožňuje nám snadno ztěžovat nebo napodobení toho, co úložiště 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 aplikace ConfigureServices:

services.AddScoped<IBloggingRepository, BloggingRepository>();

Nakonec se kontrolery vloží do služby úložiště místo kontextu EF Core a spustí na něm metody:

private readonly IBloggingRepository _repository;

public BloggingControllerWithRepository(IBloggingRepository repository)
    => _repository = repository;

[HttpGet]
public Blog GetBlog(string name)
    => _repository.GetBlogByName(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í. Tady je příklad testu založeného na napodobení pomocí oblíbené knihovny Moq :

[Fact]
public void GetBlog()
{
    // Arrange
    var repositoryMock = new Mock<IBloggingRepository>();
    repositoryMock
        .Setup(r => r.GetBlogByName("Blog2"))
        .Returns(new Blog { Name = "Blog2", Url = "http://blog2.com" });

    var controller = new BloggingControllerWithRepository(repositoryMock.Object);

    // Act
    var blog = controller.GetBlog("Blog2");

    // Assert
    repositoryMock.Verify(r => r.GetBlogByName("Blog2"));
    Assert.Equal("http://blog2.com", blog.Url);
}

Úplný vzorový kód si můžete prohlédnout tady.

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 (např. SQL Server); Podrobnosti najdete v dokumentaci zprostředkovatele SQLite. Při testování je ale obvykle vhodné použít funkci databáze V paměti 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í. V normálním využití ef Core DbContext otevře a podle potřeby zavře připojení k databázi – při každém spuštění dotazu – aby se zabránilo zbytečnému uchovávání připojení. 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 teď můžou volat CreateContext, což vrací 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 si můžete prohlédnout zde.

Zprostředkovatel v paměti

Jak je popsáno na stránce s přehledem testování, důrazně nedoporučujeme používat zprostředkovatele v paměti pro testování; 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();
}

Tady si můžete prohlédnout úplný vzorový kód pro testování v paměti.

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 InMemoryDatabaseRootUseInMemoryDatabase pro všechny kontexty, které by měly sdílet databáze v paměti (obvykle se to provádí pomocí statického InMemoryDatabaseRoot pole).

Transakce

Všimněte si, že pokud je transakce spuštěna, poskytovatel 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.

Zobrazení

Zprostředkovatel 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 }));