Delen via


Testen zonder uw productiedatabasesysteem

Op deze pagina bespreken we technieken voor het schrijven van geautomatiseerde tests die geen gebruik maken van het databasesysteem waarop de toepassing in de productieomgeving draait, door uw database te vervangen door een testdouble. Er zijn verschillende soorten test-dubbels en benaderingen om dit te doen, en het wordt aanbevolen om grondig De keuze van een teststrategie te lezen om de verschillende opties volledig te begrijpen. Ten slotte is het ook mogelijk om te testen op uw productiedatabasesysteem; dit wordt behandeld in Testen op basis van uw productiedatabasesysteem.

Fooi

Op deze pagina ziet u xUnit technieken, maar vergelijkbare concepten bestaan in andere testframeworks, waaronder NUnit.

Repository-patroon

Als u hebt besloten om tests te schrijven zonder uw productiedatabasesysteem te gebruiken, is de aanbevolen techniek hiervoor het patroon van de opslagplaats; Zie deze sectievoor meer achtergrondinformatie. De eerste stap bij het implementeren van het repositorypatroon is het extraheren van uw EF Core LINQ-query's naar een afzonderlijke laag, die we later zullen vervangen door een stub of mock. Hier volgt een voorbeeld van een opslagplaatsinterface voor ons blogsysteem:

public interface IBloggingRepository
{
    Task<Blog> GetBlogByNameAsync(string name);

    IAsyncEnumerable<Blog> GetAllBlogsAsync();

    void AddBlog(Blog blog);

    Task SaveChangesAsync();
}

... en hier volgt een gedeeltelijke voorbeeld-implementatie voor productiegebruik:

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...
}

Er is niet veel aan te doen: de opslagplaats verpakt gewoon een EF Core-context en maakt methoden beschikbaar waarmee de databasequery's en updates worden uitgevoerd. Een belangrijk punt om op te merken is dat onze GetAllBlogs methode IAsyncEnumerable<Blog> (of IEnumerable<Blog>) retourneert en niet IQueryable<Blog>. Het retourneren van het resultaat zou betekenen dat queryoperators nog steeds kunnen worden samengesteld over het resultaat, waardoor EF Core nog steeds betrokken is bij het vertalen van de query; dit zou het doel van het hebben van een repository in de eerste plaats tenietdoen. Met IAsyncEnumerable<Blog> kunnen we eenvoudig stubs of mocks maken van wat de repository retourneert.

Voor een ASP.NET Core-toepassing moeten we de opslagplaats registreren als een service in afhankelijkheidsinjectie door het volgende toe te voegen aan de ConfigureServicesvan de toepassing:

services.AddScoped<IBloggingRepository, BloggingRepository>();

Ten slotte worden onze controllers geïnjecteerd met de opslagplaatsservice in plaats van de EF Core-context en voeren ze methoden uit:

private readonly IBloggingRepository _repository;

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

[HttpGet]
public async Task<Blog> GetBlog(string name)
    => await _repository.GetBlogByNameAsync(name);

Op dit moment is uw toepassing ontworpen volgens het opslagplaatspatroon: het enige contactpunt met de gegevenstoegangslaag EF Core is nu via de opslagplaatslaag, die fungeert als een bemiddelaar tussen toepassingscode en werkelijke databasequery's. Tests kunnen nu eenvoudig worden geschreven door de opslagplaats te vervangen door een stub of door hem te simuleren met uw favoriete mockingbibliotheek. Hier volgt een voorbeeld van een op mock gebaseerde test met behulp van de populaire Moq-bibliotheek:

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

De volledige voorbeeldcode kan hier worden weergegeven.

SQLite in het geheugen

SQLite kan eenvoudig worden geconfigureerd als de EF Core-provider voor uw testsuite in plaats van uw productiedatabasesysteem (e.g. SQL Server); raadpleeg de documentatie voor de SQLite-provider voor meer informatie. Het is echter meestal een goed idee om de in-memory database van SQLite te gebruiken functie bij het testen, omdat het eenvoudige isolatie tussen tests biedt en geen gebruik hoeft te maken van werkelijke SQLite-bestanden.

Als u SQLite in het geheugen wilt gebruiken, is het belangrijk om te begrijpen dat er een nieuwe database wordt gemaakt wanneer een verbinding op laag niveau wordt geopend en dat deze wordt verwijderd wanneer deze verbinding wordt gesloten. Bij normaal gebruik opent en sluit EF Core's DbContext databaseverbindingen indien nodig - elke keer als er een query wordt uitgevoerd - om te voorkomen dat de verbindingen onnodig lang open blijven. Met SQLite in het geheugen zou dit echter ertoe leiden dat de database elke keer opnieuw wordt ingesteld; als tijdelijke oplossing openen we de verbinding voordat deze wordt doorgegeven aan EF Core en zorgen we ervoor dat deze alleen wordt gesloten wanneer de test is voltooid:

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

Tests kunnen nu CreateContextaanroepen, dat een context retourneert met behulp van de verbinding die we in de constructor hebben ingesteld, zodat we een schone database hebben met de voorgevulde gegevens.

De volledige voorbeeldcode voor SQLite in-memory testen kan hier worden bekeken.

Provider in het geheugen

Zoals besproken op de overzichtspagina voor testen, wordt het gebruik van de in-memory provider voor testen sterk afgeraden; in plaats daarvan SQLite gebruikenof het opslagplaatspatroonimplementeren. Als u hebt besloten om in-memory te gebruiken, is dit een typische testklasseconstructor waarmee een nieuwe in-memory database wordt ingesteld en gezaaid voor elke test:

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

De volledige voorbeeldcode voor in-memory testen kan hier worden weergegeven.

Naamgeving van in-memory database

In-memory databases worden geïdentificeerd met een eenvoudige tekenreeksnaam en het is mogelijk om meerdere keren verbinding te maken met dezelfde database door dezelfde naam op te geven (daarom moet het bovenstaande voorbeeld EnsureDeleted aanroepen vóór elke test). Houd er echter rekening mee dat in-memory databases zijn geroot in de interne serviceprovider van de context; hoewel contexten in de meeste gevallen dezelfde serviceprovider delen, kan het gebruik van een nieuwe interne serviceprovider worden geactiveerd door het configureren van contexten met verschillende opties. Als dat het geval is, geeft u expliciet dezelfde instantie van InMemoryDatabaseRoot door aan UseInMemoryDatabase voor alle contexten die databases in het geheugen moeten delen (dit wordt meestal gedaan door een statisch InMemoryDatabaseRoot veld).

Transacties

Als een transactie wordt gestart, genereert de provider in het geheugen standaard een uitzondering omdat transacties niet worden ondersteund. U kunt transacties op de achtergrond laten negeren door EF Core te configureren om InMemoryEventId.TransactionIgnoredWarning te negeren, zoals in het bovenstaande voorbeeld. Als uw code echter daadwerkelijk afhankelijk is van transactionele semantiek, bijvoorbeeld afhankelijk van het terugdraaien van wijzigingen, werkt uw test niet.

Weergaven

De in-memoryprovider maakt het mogelijk om weergaven te definiëren via LINQ-query's, met behulp van ToInMemoryQuery:

modelBuilder.Entity<UrlResource>()
    .ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));