Testen ohne Produktionsdatenbanksystem

Auf dieser Seite besprechen wir Techniken zum Schreiben automatisierter Tests, die das Datenbanksystem, auf dem die Anwendung in der Produktion läuft, nicht einbeziehen, indem Sie Ihre Datenbank mit einem Test-Double austauschen. Es gibt verschiedene Arten von Test-Doubles und Ansätze, um dies zu tun, und es wird empfohlen, Auswahl einer Teststrategie gründlich zu lesen, um die verschiedenen Optionen vollständig zu verstehen. Schließlich ist es auch möglich, das Produktionsdatenbanksystem zu testen. Dies wird in Tests mit Ihrem Produktionsdatenbanksystem behandelt.

Tipp

Diese Seite zeigt xUnit-Techniken, aber ähnliche Konzepte sind in anderen Testframeworks vorhanden, einschließlich NUnit.

Repositorymuster

Wenn Sie sich entschieden haben, Tests ohne Einbeziehung Ihres Produktionsdatenbanksystems zu schreiben, ist die empfohlene Technik dafür das Repositorymuster. Weitere Informationen hierzu finden Sie in diesem Abschnitt. Der erste Schritt bei der Implementierung des Repository-Musters besteht darin, Ihre EF Core LINQ-Abfragen in eine separate Schicht auszulagern, die wir später als Stub oder Mock verwenden werden. Hier ist ein Beispiel für eine Repositoryschnittstelle für unser Bloggingsystem:

public interface IBloggingRepository
{
    Blog GetBlogByName(string name);

    IEnumerable<Blog> GetAllBlogs();

    void AddBlog(Blog blog);

    void SaveChanges();
}

... und hier ist eine partielle Beispielimplementierung für die Produktionsverwendung:

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

Es gibt nicht viel zu tun: Das Repository umhüllt einfach einen EF Core-Kontext und stellt Methoden zur Verfügung, die Datenbankabfragen und -aktualisierungen ausführen. Ein wichtiger Hinweis ist, dass unsere GetAllBlogs-Methode IEnumerable<Blog> und nicht IQueryable<Blog>zurückgibt. Die Rückgabe des letzteren würde bedeuten, dass Abfrageoperatoren immer noch über das Ergebnis komponiert werden können, so dass EF Core immer noch an der Übersetzung der Abfrage beteiligt ist. Dies würde den Zweck eines Repositorys zunichte machen. IEnumerable<Blog> ermöglicht es uns, einfach als Stub oder Mock zu verwenden, was das Repository zurückgibt.

Für eine ASP.NET Core-Anwendung müssen wir das Repository als Dienst beim Einfügen von Abhängigkeiten registrieren, indem wir Folgendes zur ConfigureServices der Anwendung hinzufügen:

services.AddScoped<IBloggingRepository, BloggingRepository>();

Schließlich werden unsere Controller mit dem Repository-Service anstelle des EF Core-Kontextes verknüpft und führen Methoden auf ihm aus:

private readonly IBloggingRepository _repository;

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

[HttpGet]
public Blog GetBlog(string name)
    => _repository.GetBlogByName(name);

An diesem Punkt ist Ihre Anwendung nach dem Repositorymuster aufgebaut: Der einzige Berührungspunkt mit der Datenzugriffsschicht – EF Core – erfolgt nun über die Repositoryebene, die als Vermittler zwischen dem Anwendungscode und den eigentlichen Datenbankabfragen fungiert. Sie können jetzt Tests schreiben, indem Sie das Repository einfach per Stub auslagern oder es mit Ihrer bevorzugten Mocking-Bibliothek simulieren. Hier ist ein Beispiel für einen simulierten Test mit der beliebten Moq-Bibliothek:

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

Der vollständige Beispielcode kann hier angezeigt werden.

SQLite In-Memory

SQLite kann problemlos als EF Core-Anbieter für Ihre Testsuite konfiguriert werden, anstelle Ihres produktiven Datenbanksystems (z. B. SQL Server); Einzelheiten dazu finden Sie in der Dokumentation zum SQLite-Anbieter. In der Regel ist es jedoch eine gute Idee, beim Testen die In-Memory-Datenbankfunktion von SQLite zu verwenden, da sie eine einfache Isolierung zwischen den Tests ermöglicht und keinen Umgang mit tatsächlichen SQLite-Dateien erfordert.

Für die Verwendung von In-Memory-SQLite ist es wichtig zu verstehen, dass eine neue Datenbank erstellt wird, wenn eine Low-Level-Verbindung geöffnet wird, und dass sie gelöscht wird, wenn diese Verbindung geschlossen wird. Bei normaler Verwendung öffnet und schließt EF Cores DbContext die Datenbankverbindungen nach Bedarf – jedes Mal, wenn eine Abfrage ausgeführt wird – um zu vermeiden, dass die Verbindung unnötig lange gehalten wird. Bei In-Memory-SQLite würde dies jedoch dazu führen, dass die Datenbank jedes Mal zurückgesetzt wird. Daher öffnen wir die Verbindung, bevor wir sie an EF Core übergeben, und sorgen dafür, dass sie erst geschlossen wird, wenn der Test abgeschlossen ist:

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

Die Tests können nun CreateContext aufrufen, der einen Kontext zurückgibt, der die Verbindung verwendet, die wir im Konstruktor eingerichtet haben.

Den vollständigen Beispielcode für SQLite In-Memory-Tests finden Sie hier.

In-Memory-Anbieter

Wie auf der Testübersichtsseiteerläutert, wird die Verwendung des Speicheranbieters für Tests dringend abgeraten; erwägen Sie stattdessen die Verwendung von SQLiteoder die Implementierung des Repositorymusters. Wenn Sie sich für die Verwendung von In-Memory entschieden haben, finden Sie hier einen typischen Konstruktor für eine Testklasse, der vor jedem Test eine neue In-Memory-Datenbank einrichtet und startet:

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

Den vollständigen Beispielcode für In-Memory-Tests finden Sie hier.

Benennung der In-Memory-Datenbank

In-Memory-Datenbanken werden durch einen einfachen, Zeichenfolgennamen identifiziert, und es ist möglich, mehrmals eine Verbindung mit derselben Datenbank herzustellen, indem sie denselben Namen angeben (aus diesem Grund muss das obige Beispiel vor jedem Test EnsureDeleted aufrufen). Beachten Sie jedoch, dass In-Memory-Datenbanken im internen Dienstanbieter des Kontexts verwurzelt sind. Während Kontexte in den meisten Fällen denselben Dienstanbieter nutzen, kann die Konfiguration von Kontexten mit anderen Optionen die Verwendung eines neuen internen Dienstanbieters auslösen. Wenn dies der Fall ist, übergeben Sie explizit dieselbe Instanz von InMemoryDatabaseRoot an UseInMemoryDatabase für alle Kontexte, die In-Memory-Datenbanken freigeben sollen (dies geschieht in der Regel durch ein statisches InMemoryDatabaseRoot Feld).

Transaktionen

Wenn eine Transaktion gestartet wird, löst der Speicheranbieter standardmäßig eine Ausnahme aus, da Transaktionen nicht unterstützt werden. Sie können stattdessen Transaktionen einfach ignorieren, indem Sie EF Core so konfigurieren, dass es sie InMemoryEventId.TransactionIgnoredWarning ignoriert, wie in dem obigen Beispiel. Wenn Ihr Code jedoch tatsächlich auf transaktionsbezogene Semantik basiert – z. B. von Rollbacks, die tatsächlich zurückgesetzt werden – funktioniert ihr Test nicht.

Ansichten

Der In-Memory-Anbieter ermöglicht die Definition von Ansichten über LINQ-Abfragen mithilfe von ToInMemoryQuery:

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