Im Gegensatz zu Unit Tests werden mit Integrationstests komplette Funktionalitäten getestet. Verwendete Systeme (Datenbanken etc.) müssen für Tests entsprechend konfiguriert sein und zur Verfügung stehen.

Nehmen wir als Beispiel eine Web API. Diese gibt definierte Endpunkte nach außen. Ein Client (Browser, Mobilgerät etc.) kann diese Endpunkte bedienen und darüber Informationen abfragen oder übermitteln. Integrationstests fungieren als Client. Der Unterschied ist, dass Ergebnisse zu erwartenden Ergebnissen gegenübergestellt werden. So kann entschieden werden, ob alle APIs korrekt funktionieren.

Wie auch bei Unit Tests sind nicht nur Schönwetter-Fälle zu testen. Es ist mit Fehleingaben zu rechnen, wie geht das System damit um? Stürzt es ab, oder liefert es dem Client eine Information darüber, warum eine Anfrage nicht angenommen wurde oder kein Ergebnis geliefert hat?

Testprojekt erstellen

Welche Schritte sind nun notwendig, um Integrationstests unter .NET Core ausführen zu können?

Im ersten Schritt wird ein neues Testprojekt erstellt:

Visual Studio .NET Core xUnit Test Projekt | Norbert Eder

Visual Studio .NET Core xUnit Testprojekt erstellen

Zur Auswahl steht die Möglichkeit einer MSTest-Umgebung oder der Verwendung von xUnit. In diesem Fall wurde das xUnit-Projekt gewählt. Nach der Erstellung des Projektes muss ein Rebuild vorgenommen werden, damit alle notwendigen Abhängigkeiten bezogen werden. Dies kann via nuget restore auch ohne Rebuild durchgeführt werden.

Im nächsten Schritt ist das NuGet-Package Microsoft.AspNetCore.TestHost dem Projekt hinzuzufügen. Hiermit wird unter anderem die Klasse TestServer zur Verfügung gestellt. Damit kann eine komplette Server-Instanz hochgezogen werden (ohne IIS etc.):

public class EndpointTest
{
    private readonly TestServer server;
    private readonly HttpClient client;

    public EndpointTest()
    {
        var webHostBuilder =
            new WebHostBuilder()
                .UseEnvironment("Test")
                // Startup-Klasse des eigentlichen Projektes
                .UseStartup<Startup>();

        this.server = new TestServer(webHostBuilder);
        this.client = server.CreateClient();            
    }

    [Fact]
    public async void ConnectToEndpoint_ShouldBeOk()
    {
        string result = await client.GetStringAsync("/api/endpoint");
        Assert.Equal("[RESPONSE]", result);
    }
}

In diesem Beispiel wird im Konstruktor der Testserver mit der Startup-Klasse des Projektes hochgezogen. Damit werden alle im Startup angegebenen Konfigurationen verwendet. In den Testmethoden werden nun auf die einzelnen Endpunkte Abfragen abgesetzt und das Ergebnis geprüft.

Ausführen der Integrationstests

Die Tests können über Visual Studio ausgeführt werden. Alternativ dazu ist auch die Ausführung in der Konsole möglich:

dotnet test

Weiterführende Informationen können in der Dokumentation zu dotnet test gefunden werden.

Bei Unit Tests ist es wichtig, dass jeder Test unabhängig der anderen Tests ausgeführt werden kann (und auch funktioniert). Integrationstests müssen hingegen oft in einer definierten Reihenfolge ausgeführt werden. So ist es in der Regel erst möglich Daten abzufragen, nachdem eine Anmeldung am System erfolgte. Zur Veranschaulichung möchte ich ein kleines Beispiel aufzeigen:

  1. Anmelden am System
  2. Anlage eines Kunden
  3. Anlage eines Kundenprojektes
  4. Aktualisieren eines Kundenprojektes
  5. Aktualisieren des Kunden
  6. Löschversuch des Kunden
  7. Löschen eines Kundenprojektes
  8. Löschen des Kundens
  9. Abmelden vom System

Diese und viele weitere Schritte müssen unternommen werden, um die Funktionsweise, Stabilität und Konsistenz der Software zu garantieren.

Bei der Verwendung von xUnit ist es möglich, einen Reihenfolgen-Mechanismus zu verwenden. Hierzu wird die Schnittstelle ITestCaseOrderer zur Verfügung gestellt.

Reihenfolge per Attribut steuern

Damit die gewünschte Reihenfolge gesetzt werden kann, ist ein Attribut zu implementieren und zu verwenden:

public class TestPriorityAttribute : Attribute
{
    public int Priority { get; set; }

    public TestPriorityAttribute(int priority)
    {
        Priority = priority;
    }
}

Gesetzt wird die Priorität dann so:

[Fact, TestPriority(20)]
public async void ConnectToEndpoint_ShouldBeOk()
{
    string result = await client.GetStringAsync("/api/1/endpoint");
    Assert.Equal("[RESPONSE]", result);
}

Orderer implementieren und verwenden

Der nachfolgende Orderer implementiert die Schnittstelle ITestCaseOrderer und sortiert alle Testfälle innerhalb einer Testklasse nach deren gesetzten Priorität:

public class TestPriorityOrderer : ITestCaseOrderer
{
    public IEnumerable<TTestCase> OrderTestCases<TTestCase>(IEnumerable<TTestCase> testCases) where TTestCase : ITestCase
    {
        SortedList<int, TTestCase> sortedTestCases = new SortedList<int, TTestCase>();
        foreach (var testCase in testCases)
        {
            var methodInfo = testCase.TestMethod.Method;
            var attribute = methodInfo.GetCustomAttributes((typeof(TestPriorityAttribute).AssemblyQualifiedName)).FirstOrDefault();
            var priority = attribute.GetNamedArgument<int>("Priority");
            sortedTestCases.Add(priority, testCase);
        }
        return sortedTestCases.Values.ToList();
    }
}

Beachte bitte, dass dies für die Reihenfolge der Testfälle innerhalb einer Testklasse gilt. Dies gilt nicht für die Reihenfolge von Testklassen. Dafür gibt es die Schnittstelle ITestCollectionOrderer. Dies funktioniert analog zum gezeigten Beispiel.

In der jeweiligen Testklasse ist der zu verwendende Orderer als Attribut zu setzen:

[TestCaseOrderer("MyProject.IntegrationTests.TestPriorityOrderer", "MyProject.IntegrationTests")]
public class EndpointTest
{
    // Code goes here
}

Ab sofort wird dieser ausgerufen und alle Testfällt entsprechend sortiert.

Weitere Schritte

In komplexeren Umgebungen empfiehlt es sich, entsprechende Abstraktionen zu schaffen. Eventuell ist es auch sinnvoll, von der Startup abzuleiten, um an der Middleware zu schrauben.

Eine Integration ins Buildsystem ist unbedingt in Erwägung zu ziehen. Können die Tests, trotz der Beteiligung von weiteren Systemen, in kurzer Zeit durchlaufen, sollten diese durchgeführt werden, bevor der Sourcecode weiteren Entwicklern (über ein Source Control) zur Verfügung gestellt wird.

Fazit

Integrationsmittel sind ein hervorragendes Mittel, um den aktuellen Zustand eines Systems zu ermitteln. Das Aussagekraft ist jedoch nur so gut, wie die vorhandenen Tests. Tests sollten während der Entwicklung und bei Bekanntwerden von Problemen erstellt werden. Dadurch wächst die Testbasis ständig an und hilft Probleme frühzeitig zu erkennen bzw. wiederholte Fehler zu vermeiden. Ich empfehle, sowohl Unit Tests als auch Integration Tests von Beginn an zu verwenden. Ein hinausgezögerter Einbau findet in der Regel nie statt, oder wird dann getriggert, wenn die Kacke bereits am Dampfen ist. Viel Spaß beim Erschaffen einer qualitativ hochwertigen Lösung!

Happy Coding.

Über den Autor

Norbert Eder

Ich bin ein leidenschaftlicher Softwareentwickler und Fotograf. Mein Wissen und meine Gedanken teile ich nicht nur hier im Blog, sondern auch in Fachartikeln und Büchern.