Komponenten- und Integrationstests in Minimal-API-Apps

Von Fiyaz Bin Hasan und Rick Anderson

Einführung in Integrationstests

Integrationstests bewerten die Komponenten einer App auf breiterer Ebene als Komponententests. Komponententests werden verwendet, um isolierte Softwarekomponenten wie z. B. einzelne Klassenmethoden zu testen. Integrationstests bestätigen, dass zwei oder mehr App-Komponenten zusammenarbeiten, um ein erwartetes Ergebnis zu erzielen, ggf. auch unter Einbindung aller Komponenten, die für die vollständige Verarbeitung einer Anforderung erforderlich sind.

Diese umfassenderen Tests werden verwendet, um die Infrastruktur und das gesamte Framework der App zu testen, häufig einschließlich der folgenden Komponenten:

  • Datenbank
  • Dateisystem
  • Netzwerk-Appliances
  • Anforderung/Antwort-Pipeline

Komponententests verwenden anstelle von Infrastrukturkomponenten künstliche Komponenten, die als Fakes oder Pseudoobjekte bezeichnet werden.

Im Gegensatz zu Komponententests gilt für Integrationstests:

  • Sie verwenden die tatsächlichen Komponenten, die von der App in der Produktionsumgebung verwendet werden.
  • Sie erfordern mehr Code und Datenverarbeitung.
  • Ihre Ausführung dauert länger.

Beschränken Sie daher die Verwendung von Integrationstests auf die wichtigsten Infrastrukturszenarios. Wenn ein Verhalten mithilfe eines Komponententests oder eines Integrationstests getestet werden kann, wählen Sie den Komponententest.

Bei der Besprechung von Integrationstests wird das getestete Projekt häufig als getestetes System oder kurz GS (englisch System Under Test, SUT) bezeichnet. In diesem Artikel wird zum Verweis auf die zu testende ASP.NET Core-Anwendung der Begriff „GS“ verwendet.

Schreiben Sie keine Integrationstests für jede Permutation des Daten- und Dateizugriffs bei Datenbanken und Dateisystemen. Unabhängig davon, wie viele Elemente in einer App mit Datenbanken und Dateisystemen interagieren, ist ein fokussierter Satz von Lese-, Schreib-, Update- und Lösch-Integrationstests üblicherweise in der Lage, die Datenbank- und Dateisystemkomponenten adäquat zu testen. Verwenden Sie Komponententests für Routinetests der Methodenlogik, die mit diesen Komponenten interagieren. Bei Komponententests beschleunigt die Verwendung von Fake- oder Pseudoergebnissen für eine Infrastruktur die Testausführung.

Integrationstests in ASP.NET Core

Integrationstests in ASP.NET Core erfordern Folgendes:

  • Es wird ein Testprojekt verwendet, um die Tests einzugrenzen und auszuführen. Das Testprojekt enthält einen Verweis auf das GS.
  • Das Testprojekt erstellt einen Testwebhost für das GS und verwendet einen Testserverclient, um Anforderungen und Antworten im Zusammenhang mit dem GS zu verarbeiten.
  • Um die Tests auszuführen und die Testergebnisse zu melden, wird ein Test-Runner verwendet.

Integrationstests folgen einer Sequenz von Ereignissen, die die üblichen Testschritte Arrange, Act und Assert umfassen:

  1. Der Webhost des GS wird konfiguriert.
  2. Es wird ein Testserverclient erstellt, um Anforderungen an die App zu senden.
  3. Der Testschritt Arrange wird ausgeführt: Die Test-App bereitet eine Anforderung vor.
  4. Der Testschritt Act wird ausgeführt: Der Client sendet die Anforderung und empfängt die Antwort.
  5. Der Testschritt Assert wird ausgeführt: Die tatsächliche Antwort wird je nach der erwarteten Antwort als Pass oder Fail bewertet.
  6. Der Prozess wird so lange fortgesetzt, bis alle Tests ausgeführt wurden.
  7. Die Testergebnisse werden gemeldet.

Üblicherweise ist der Testwebhost anders konfiguriert als der normale Webhost der App für die Testläufe. Beispielsweise könnten für die Tests eine andere Datenbank oder andere App-Einstellungen verwendet werden.

Infrastrukturkomponenten wie der Testwebhost und der In-Memory-Testserver (TestServer) werden durch das Paket Microsoft.AspNetCore.Mvc.Testing bereitgestellt oder verwaltet. Durch die Verwendung dieses Pakets werden die Testerstellung und -ausführung optimiert.

Das Microsoft.AspNetCore.Mvc.Testing-Paket verarbeitet die folgenden Aufgaben:

  • Es kopiert die Datei für Abhängigkeiten ( .deps) aus dem GS in das bin Verzeichnis des Testprojekts.
  • Es legt das Inhaltsstammelement auf das Projektstammelement des GS fest, damit statische Dateien und Seiten/Ansichten bei der Ausführung der Tests gefunden werden.
  • Es stellt die Klasse WebApplicationFactory zur Optimierung des Bootstrappings des GS mit TestServer bereit.

In der Dokumentation der Komponententests wird beschrieben, wie Sie ein Testprojekt und einen Test-Runner einrichten. Ferner finden Sie dort ausführliche Anweisungen zum Ausführen von Tests sowie Empfehlungen zum Benennen von Tests und Testklassen.

Unterteilen Sie Komponententests und Integrationstests in verschiedene Projekte. Trennen der Tests:

  • Damit wird sichergestellt, dass Komponenten für Infrastrukturtests nicht versehentlich in die Komponententests eingeschlossen werden.
  • So können Sie steuern, welche Testsätze ausgeführt werden.

Der Beispielcode auf GitHub umfasst ein Beispiel für Komponenten- und Integrationstests bei einer Minimal-API-App.

IResult-Implementierungstypen

Öffentliche IResult-Implementierungstypen im Microsoft.AspNetCore.Http.HttpResults-Namespace können verwendet werden, um Komponententests für minimale Routenhandler auszuführen, wenn anstelle von Lambdafunktionen benannte Methoden verwendet werden.

Im folgenden Code wird die NotFound<TValue>-Klasse verwendet:

[Fact]
public async Task GetTodoReturnsNotFoundIfNotExists()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    // Act
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert
    Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

    var notFoundResult = (NotFound) result.Result;

    Assert.NotNull(notFoundResult);
}

Im folgenden Code wird die Ok<TValue>-Klasse verwendet:

[Fact]
public async Task GetTodoReturnsTodoFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title",
        Description = "Test description",
        IsDone = false
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert
    Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

    var okResult = (Ok<Todo>)result.Result;

    Assert.NotNull(okResult.Value);
    Assert.Equal(1, okResult.Value.Id);
}

Zusätzliche Ressourcen