Sdílet prostřednictvím


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 ConfigureServicesaplikace:

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 }));