Wo man die Grenze zwischen Unit-Tests und Integrationstests zieht

· · Tests, Unit-Tests, Integrationstests, Testdesign, Windows-Entwicklung, C# / .NET

Bei Fragen des Testdesigns ist es jedes Mal die unauffällig schwierige Frage, wie weit man etwas in Unit-Tests drückt und ab wo man es in Integrationstests hochstuft.

Gefährlich sind hier die beiden Extreme:

  • Weil man schnell iterieren will, wird alles zum Unit-Test
  • Weil es näher am echten System ist, wird alles zum Integrationstest

Ersteres führt zu einer Flut von Mocks und übersieht leicht die Stellen, die in der Produktion tatsächlich brechen; Letzteres neigt zu einer langsamen, brüchigen Testmenge. In der Praxis sind die Achsen, auf die man achten sollte, etwas klarer.

  • Wollen Sie die eigene Logik prüfen oder die Anbindung nach außen?
  • Bleibt die Bedeutung erhalten, wenn Sie durch einen In-Memory-Fake ersetzen?
  • Ist das eigentliche Thema das Verhalten von DB / Dateien / HTTP / DI / Konfiguration / Framework / Betriebssystem?
  • Wollen Sie eine große Zahl an Eingabemustern schnell durchlaufen lassen?

Sind diese vier Punkte klar, lässt sich die Grenze zwischen Unit-Test und Integrationstest deutlich leichter ziehen.

Dieser Artikel setzt automatisiertes Testdesign in C# / .NET voraus. Die Codebeispiele verwenden xUnit, aber das Entscheidungskriterium selbst hängt nicht vom Framework ab. Es ist so gehalten, dass es sich unverändert auf JUnit oder pytest übertragen lässt.

Der Inhalt stützt sich auf die im März 2026 verfügbaren Quellen Microsoft Learn, „Integration tests in ASP.NET Core“1, ebenso „Unit testing best practices for .NET“2, sowie Martin Fowlers „The Practical Test Pyramid“3. Damit es im Folgenden nicht bei bloßen Nummernverweisen bleibt, wird der Name der Quelle auch im Fließtext genannt. Die URLs der Originalquellen sind am Artikelende unter Quellen zusammengefasst.

1. Zuerst das Fazit

Grob gesagt, aber in einer für die Praxis brauchbaren Form:

  1. Reine Logik gehört in den Unit-Test
  2. Verbindungen, Verdrahtung, Umwandlung und Umgebungsunterschiede gehören in den Integrationstest
  3. Lässt sich beides prüfen, zuerst der Unit-Test
  4. Integrationstests lieber schmal auf die Grenze fokussieren, statt sie breit und schwer zu machen

Kurz gesagt: Ein Unit-Test ist ein „Test der Entscheidung“, ein Integrationstest ein „Test der Verbindung“.

Dinge, deren Bedeutung ohne externe Ressourcen vollständig ist — Preisberechnung, Zustandsübergänge, Eingabevalidierung, Genehmigungsbedingungen, Klassifizierung von Ausnahmen —, laufen als Unit-Test schneller, sind weniger brüchig und lassen sich mit einer größeren Zahl an Eingabemustern durchlaufen. Dinge dagegen, die „im Moment der Verbindung verraten, dass sie kaputt sind“ — SQL-Ausführung, JSON-/CSV-Serialisierung, Routing, Modellbindung, DI-Registrierung, Dateisperren, Berechtigungen, COM-Registrierung, 32-Bit / 64-Bit, STA / MTA —, sind auf der Seite des Integrationstests sicherer aufgehoben.

Auch Microsoft Learns Integration tests in ASP.NET Core empfiehlt, Integrationstests auf wichtige Infrastrukturszenarien zu beschränken und den Unit-Test zu wählen, wo er ausreicht.

2. Was dieser Artikel unter Unit-Test und Integrationstest versteht

Hier werden die Begriffe wie folgt verwendet.

Ebene Was geprüft wird Typischer Aufbau
Unit-Test Korrektheit einer isolierten Verantwortlichkeit Verwendet Fake / Mock / Stub und schneidet externe Ressourcen ab
Integrationstest Verbindung mehrerer Komponenten sowie Verhalten einschließlich Infrastruktur und Framework Echte DB, echte Dateien, echter Serializer, echter Host, echte Pipeline usw.
E2E-/Funktionstest Nutzerablauf durch die gesamte Anwendung Bereitgestellte Anwendung, mehrere Dienste, echter Browser oder echter Prozess

In .NETs Aufbereitung des Unit-Testings wird ein guter Unit-Test als fast / isolated / repeatable beschrieben, der nicht von externen Faktoren wie dem Dateisystem oder einer Datenbank abhängt. Näheres dazu liefert übersichtlich Unit testing best practices for .NET.

Zudem bezeichnet ein Integrationstest nicht nur „einen schweren Test, der zwingend einen separaten Prozess oder Server verwendet“. Auch innerhalb desselben Prozesses gilt: Verbindet man mehrere echte Komponenten und prüft das tatsächliche Verhalten von Framework oder Infrastruktur, tendiert das zum Integrationstest.

Wenn man beispielsweise eine Controller-Action von ASP.NET Core mit einem Unit-Test prüft, wird offiziell empfohlen, sich auf die Entscheidungen im Rumpf der Action zu beschränken und Wechselwirkungen auf Framework-Seite wie routing, model binding und filters per Integrationstest zu behandeln. Näheres dazu ordnet Unit test controller logic in ASP.NET Core übersichtlich.

2.1. Die Unterscheidung zwischen Fake / Mock / Stub

In der Tabelle oben standen fake / mock / stub nebeneinander, aber diese drei sind nicht dasselbe. Als Klassifikation von Testdoubles (Stellvertretern für Tests) verwendet dieser Artikel die folgenden Bedeutungen — eine Unterscheidung, die sich an Martin Fowlers Aufbereitung in „Mocks Aren’t Stubs“ orientiert.4

Bezeichnung Was er tut Typischer Einsatz
Stub Ein Stellvertreter, der nur feste Werte zurückgibt. Wie er aufgerufen wird, wird nicht geprüft Wenn eine Eingabebedingung fixiert werden soll, etwa „der Lagerbestand ist immer 3 Stück“
Mock Ein Stellvertreter, der prüft, wie er selbst aufgerufen wurde — Aufrufanzahl und Argumente werden asserted Wenn geprüft werden soll, dass „Speichern genau einmal aufgerufen wird“
Fake Ein Ersatz für das gleiche Verhalten wie das echte Ding, aber mit einer leichten Implementierung Ein In-Memory-Repository, eine Dateiablage in einem temporären Verzeichnis usw.

Grob gesagt: Ein Stub ist ein Stellvertreter für Eingaben, ein Mock prüft Aufrufe, ein Fake ist eine vereinfachte Implementierung. Diese Unterscheidung wird in Kapitel 4 wichtig. Das Anzeichen „es braucht 7 Mocks“ bedeutet genau genommen, dass es 7 zu prüfende Aufrufziele gibt — ein Signal dafür, dass der Test nicht eine einzelne Entscheidung, sondern die Verkabelung mehrerer Bauteile prüfen will.

2.2. Wie dieser Artikel mit E2E umgeht

Das Thema dieses Artikels ist ausschließlich die Grenze zwischen Unit-Test und Integrationstest. E2E- / Funktionstests stehen zum Vergleich in der obigen Tabelle, werden im Fließtext aber nicht vertieft.

Nur zur Einordnung: Im Denkmodell der Testpyramide entsteht ein Dreieck, bei dem die unteren Schichten zahlreicher und schneller, die oberen Schichten spärlicher und langsamer sind. Martin Fowlers „The Practical Test Pyramid“ ist der Ausgangspunkt dieser Einteilung.3 Das Verhältnis lautet also: Unit-Tests als Fundament, Integrationstests je Grenze platziert, und E2E nur auf die wichtigsten Abläufe beschränkt. Die konkrete Aufteilung ist in der dreischichtigen Struktur in Kapitel 7 zusammengefasst.

3. Die Entscheidungstabelle auf einen Blick

Zunächst die Tabelle, die in der Praxis am nützlichsten ist.

Was Sie prüfen wollen Primäre Teststufe Anmerkung
Preisberechnung, Rabatte, Zustandsübergänge, Eingabevalidierung Unit-Test Eingabemuster sollen dicht durchlaufen werden
Klassifizierung von Ausnahmen, Auswahl der Fehlermeldung, Entscheidung über Retry Unit-Test Bedeutung ist ohne echtes I/O vollständig
SQL-/ORM-Übersetzung des Repository, Transaktionen Integrationstest Das Verhalten der echten DB oder des echten Providers ist das Thema
Serialize / Deserialize von JSON / XML / CSV Integrationstest Abweichungen im Wire-Format sind mit Fakes schwer zu finden
Routing, Modellbindung, Filter, Middleware Integrationstest Prüfung der Anbindung an das Framework
Zustandsübergänge von WPF-/WinForms-ViewModels oder Presentern Unit-Test Sinnvoll, auch ohne UI aufzubauen
Tatsächliches Binding, Dispatcher, Control-Lifecycle, Message Loop Integrationstest oder UI-Test Verhalten von Framework und Threads ist das Thema
Dateipfade, Berechtigungen, Sperren, Freigabeordner, Zeilenumbrüche, Zeichenkodierung Integrationstest Erfordert das tatsächliche Verhalten von OS und Dateisystem
COM-Registrierung, 32-Bit / 64-Bit, STA / MTA, DLL-Laden Integrationstest Umgebungsunterschiede und Prozessgrenzen sind das Thema
Start der gesamten Anwendung, durchgängige Prüfung der wichtigsten Anwendungsfälle E2E / Smoke Wenige Fälle reichen aus

Der Trick beim Lesen ist die Frage, welcher Test dem „Grund, warum es in der Produktion bricht“ am nächsten kommt. Zu entscheiden anhand der Unsicherheit, die man reduzieren will, statt anhand des Codeorts, führt zu weniger Schwankungen.

4. Was der Unit-Test abdecken sollte

Für den Unit-Test geeignet ist Verantwortlichkeit, deren Bedeutung auch ohne die Außenwelt erhalten bleibt.

Zum Beispiel:

  • Geschäftsregeln
  • Verzweigungen
  • Zustandsübergänge
  • Eingabevalidierung
  • Fehlerklassifizierung
  • Entscheidung der Retry-Strategie
  • Zustandsänderungen von ViewModel / Presenter
  • Die Konvertierungslogik selbst

Besonders gilt: Je mehr Kombinationen etwas hat, desto größer ist der Wert, es in den Unit-Test zu verschieben.

Zum Beispiel:

  • Gutschein vorhanden / nicht vorhanden
  • Lagerbestand vorhanden / nicht vorhanden
  • Erstbestellung / Wiederbestellung
  • Administrator / normaler Benutzer
  • gültiger Wert / Grenzwert / ungültiger Wert

Je mehr solcher Verzweigungsbedingungen es gibt, desto schwerer wird es, sie alle über Integrationstests abzudecken. Hier ist es sinnvoller, mit Unit-Tests fein aufzuteilen.

Zudem ist es im Unit-Test wichtig, externe Faktoren kontrollierbar zu halten.

  • Die aktuelle Zeit injizieren
  • GUIDs und Zufallszahlen austauschbar machen
  • Nicht mit sleep warten
  • Keine echte DB und keine echten Dateien anfassen
  • Nicht ins echte Netzwerk hinausgehen

Werden diese Punkte eingehalten, werden die Tests recht stabil.

4.1. Wenn im Unit-Test zu viele Mocks anfallen

Wenn beim Schreiben eines Unit-Tests

  • 7 Mocks nötig werden,
  • das Setup lang wird,
  • der Arrange-Teil länger als der Testkörper ist, oder
  • nicht mehr erkennbar ist, was eigentlich geprüft werden soll,

liegt meist eines von beidem vor:

  1. Die Klasse hat zu viele Verantwortlichkeiten
  2. Eine Verdrahtung, die eigentlich per Integrationstest geprüft werden sollte, wurde in den Unit-Test gedrückt

Ein Mock ist ein Werkzeug, um die Außenwelt abzuschneiden — kein Werkzeug, um zu beweisen, dass die Verbindung zum echten Ding korrekt ist. Verwechselt man das, entsteht leicht die Situation, dass „alles grün ist, aber die Produktion trotzdem abstürzt“.

4.2. Minimalbeispiel für einen Unit-Test

In Worten allein bleibt das abstrakt, deshalb wird hier je ein Beispiel für beide Seiten der Grenze im Code gezeigt. Zuerst die Seite des Unit-Tests. Der Zustandsübergang eines ViewModels ist auch ohne eine aufgebaute UI vollständig sinnvoll — das ist hier die Domäne des Unit-Tests.

// .NET 8 / xUnit
// Ziel: ein ViewModel, das keinerlei externe Ressourcen anfasst
public sealed class OrderViewModel
{
    public decimal Subtotal { get; set; }
    public bool IsMember { get; set; }

    public bool CanCheckout => Subtotal > 0m;
    public decimal Total => IsMember ? Subtotal * 0.9m : Subtotal;
}

public class OrderViewModelTests
{
    [Fact]
    public void MitgliedErhaeltZehnProzentRabatt()
    {
        var viewModel = new OrderViewModel { Subtotal = 1000m, IsMember = true };

        Assert.Equal(900m, viewModel.Total);
    }

    [Theory]
    [InlineData(0, false)]
    [InlineData(1, true)]
    public void BeiSubtotalNullKeinCheckout(int subtotal, bool expected)
    {
        var viewModel = new OrderViewModel { Subtotal = subtotal };

        Assert.Equal(expected, viewModel.CanCheckout);
    }
}

Hier tauchen weder DB noch Datei noch HTTP auf. Deshalb läuft das schnell, parallelisierbar, und mit [Theory] lassen sich beliebig viele Eingabemuster ergänzen. Die vollständige Abdeckung aller Verzweigungen hierher zu verschieben meint konkret genau diese Form.

5. Die vier Grenzen, die in den Integrationstest gehören

Die Stellen, die in den Integrationstest hochgestuft werden sollten, lassen sich grob in vier Kategorien einteilen: Format, Verdrahtung, Umgebung und Zeit.

5.1. Die Format-Grenze

Zum Format gehören hier Dinge wie:

  • JSON / XML / CSV
  • Schema und Mapping der DB
  • nullable / Präzision / Zeitzone
  • Serialisierung von Enums und Datumswerten
  • Zeichenkodierung und BOM
  • Zeilenumbruchzeichen

Auch Martin Fowler nennt Grenzen, an denen Serialize / Deserialize stattfindet, als Kandidaten für Integrationstests. Näheres dazu bietet The Practical Test Pyramid.

Fehler wie:

  • Ein DTO wurde zu JSON serialisiert und die Feldnamen stimmten nicht
  • Anführungszeichen oder Zeilenumbrüche im CSV wurden zerstört
  • Ein decimal wurde gerundet
  • Die Behandlung von DateTimeOffset in der DB wich ab
  • null und ein leerer String wurden anders behandelt als erwartet

schlüpfen mit reinen Unit-Tests leicht durch.

5.2. Die Verdrahtungsgrenze

Zur Verdrahtungsgrenze gehören zum Beispiel Teile wie diese:

  • DI-Registrierung
  • Bindung der Konfiguration
  • Routing
  • Modellbindung
  • Filter
  • Middleware
  • Start des Hosts
  • Event-Verdrahtung
  • WPF-Binding und Command-Anbindung

Hier geht es nicht um „ist meine eigene Funktion korrekt“, sondern darum, ob mehrere echte Bauteile korrekt miteinander verbunden sind.

In ASP.NET Core gibt es die offizielle Einteilung, den Unit-Test einer Controller-Action auf deren Entscheidungen zu beschränken und routing, model binding sowie filters auf der Seite des Integrationstests zu betrachten. Der Gedanke ist auch außerhalb des Web derselbe: Auch in einer Desktop-Anwendung gehören Zustandsübergänge des ViewModels in den Unit-Test, während Verhalten mit tatsächlichem XAML-Binding oder Dispatcher eher zum Integrationstest tendiert.

5.3. Die Umgebungsgrenze

In der Windows-Entwicklung ist dieser Punkt ziemlich wichtig.

  • Dateiberechtigungen
  • Freigabeordner
  • Dateisperren
  • Rename von einer temporären Datei aus
  • Administratorrechte
  • Rechte zum Starten von Diensten
  • COM-Registrierung
  • 32-Bit / 64-Bit
  • STA / MTA
  • Ladequelle der DLL

Hier sind die Bedingungen des Betriebssystems oder der Ausführungsumgebung selbst die Hauptrolle. Mit einem In-Memory-Fake geht die Bedeutung erheblich verloren, weshalb es sicherer ist, dies mit Integrationstests abzudecken.

Besonders in Konfigurationen mit bestehender Windows-Software oder COM / ActiveX ist es ganz normal, noch vor der Logik über Registrierung, Bitness, Threading-Modell und Berechtigungen zu stolpern. Solche Fehler werden nicht vom Unit-Test, sondern von einem die Umgebung einschließenden Integrationstest aufgefangen.

5.4. Die Zeitgrenze

Ein weiterer leicht übersehener Punkt sind Zeit und Nebenläufigkeit.

  • Timeout
  • Cancellation
  • Tatsächliches Verhalten von Retry
  • Timer-gesteuerte Verarbeitung
  • Stoppen von Hintergrundverarbeitung
  • Race Condition
  • Reihenfolge des Beendens beim Shutdown

Wichtig ist hier, Entscheidung und tatsächliches Verhalten zu trennen.

Zum Beispiel reichen für

  • wie oft retryt wird und
  • welche Ausnahmen für Retry infrage kommen

Unit-Tests völlig aus. Andererseits tendieren

  • ob der Timeout tatsächlich greift,
  • ob Cancellation propagiert,
  • ob nichts kaputtgeht, wenn Timer und asynchrone Verarbeitung kollidieren, und
  • ob Handles und Tasks beim Beenden sauber geschlossen werden

zum Integrationstest.

5.5. Minimalbeispiel für einen Integrationstest

Am gleichen Thema wie in 4.2 wird nun die andere Seite der Grenze gezeigt. Geprüft werden soll hier, ob „der gespeicherte Betrag den Hin- und Rückweg durch die DB unbeschadet übersteht“. Dazu läuft der Test über eine echte SQLite-Datei.

// .NET 8 / xUnit / Microsoft.Data.Sqlite
using System.Globalization;
using Microsoft.Data.Sqlite;

public sealed class OrderRepository(SqliteConnection connection)
{
    public void Save(int id, decimal total)
    {
        using var command = connection.CreateCommand();
        command.CommandText = "INSERT INTO orders (id, total) VALUES ($id, $total);";
        command.Parameters.AddWithValue("$id", id);
        command.Parameters.AddWithValue("$total", total.ToString(CultureInfo.InvariantCulture));
        command.ExecuteNonQuery();
    }

    public decimal FindTotal(int id)
    {
        using var command = connection.CreateCommand();
        command.CommandText = "SELECT total FROM orders WHERE id = $id;";
        command.Parameters.AddWithValue("$id", id);
        var stored = (string)command.ExecuteScalar()!;
        return decimal.Parse(stored, CultureInfo.InvariantCulture);
    }
}

public sealed class OrderRepositoryTests : IDisposable
{
    private readonly string _databasePath =
        Path.Combine(Path.GetTempPath(), $"orders-{Guid.NewGuid():N}.db");
    private readonly SqliteConnection _connection;

    public OrderRepositoryTests()
    {
        // Ohne Pooling=False lässt sich die DB-Datei beim Aufräumen manchmal nicht löschen
        _connection = new SqliteConnection($"Data Source={_databasePath};Pooling=False");
        _connection.Open();

        using var create = _connection.CreateCommand();
        create.CommandText = "CREATE TABLE orders (id INTEGER PRIMARY KEY, total TEXT NOT NULL);";
        create.ExecuteNonQuery();
    }

    [Fact]
    public void GespeicherterBetragWirdOhneRundungZurueckgelesen()
    {
        var repository = new OrderRepository(_connection);

        repository.Save(id: 1, total: 1234.56m);

        Assert.Equal(1234.56m, repository.FindTotal(1));
    }

    public void Dispose()
    {
        _connection.Dispose();
        File.Delete(_databasePath);
    }
}

Was dieser Test prüft, ist nicht die Verzweigung von OrderRepository. Es ist der Verbindungsteil: Geht das SQL durch, passt die Zuordnung zwischen Spaltentyp und decimal, und bleibt der Wert nach dem Hin- und Rückweg erhalten? Da SQLite keinen decimal-Typ kennt, wird „in welchem Typ gespeichert werden muss, damit der Wert den Hin- und Rückweg übersteht“ nicht zu einer Frage der Implementierung, sondern zu einer Frage des Verbindungsdesigns. Das ist mit einem In-Memory-Fake nicht sichtbar.

In diesem Beispiel wird pro Testklasse eine temporäre DB-Datei angelegt und in Dispose wieder gelöscht. Da Integrationstests Zustand tragen, ist es hier wichtiger als beim Unit-Test, jedes Mal klarzustellen, wo etwas angelegt und wo es wieder verworfen wird.

6. Häufige Fehleinschätzungen

6.1. Sich mit gemocktem Repository zufriedengeben

Auch wenn alles rund um das Repository per Mock durchläuft, weiß man dadurch nicht:

  • ob das SQL korrekt ist,
  • ob die Transaktion greift,
  • ob es mit dem Schema übereinstimmt,
  • ob das Mapping nicht abweicht, oder
  • ob Zeichenkodierung und Präzision nicht kaputtgehen.

Ein Repository ist häufig weniger ein Testziel für Logik als vielmehr ein Verbindungspunkt an einer Grenze. In diesem Fall entspricht es der Realität eher, das Gewicht statt auf Unit-Tests auf Integrationstests zu legen.

6.2. Beim Unit-Test von Controller / Endpoint versuchen, auch das Framework zu prüfen

Was man im Unit-Test einer Controller-Action sehen will, ist ungefähr:

  • die bedingte Verzweigung,
  • die Wahl des Rückgabewerts,
  • welcher abhängige Dienst jeweils aufgerufen wird.

Andererseits gehören

  • ob die Route trifft,
  • ob das Model Binding durchgeht,
  • ob der Filter greift, und
  • wie das Ergebnis nach dem Durchlauf durch die Middleware aussieht

auf die Seite des Integrationstests. Vermischt man das, wird schwer erkennbar, was eigentlich kaputt ist.

6.3. Im Integrationstest alle Eingabemuster durchprobieren

Integrationstests sind, da sie näher am echten System sind, zwangsläufig langsamer. Deshalb lohnt es sich, zu trennen: vollständige Abdeckung der Verzweigungen im Unit-Test, repräsentative Fälle der Grenze im Integrationstest.

Auch Microsoft Learns Erläuterung zu Integrationstests empfiehlt, bei DB und Dateisystem nicht alle Muster per Integrationstest durchlaufen zu lassen, sondern sich auf repräsentative Szenarien wie read / write / update / delete zu beschränken.

6.4. Von der CI aus direkt die Produktivsysteme externer Dienste ansprechen

Das ist besser zu vermeiden.

Bei Integrationstests ist „Echtheit“ wichtig, aber das heißt nicht, dass bei jedem Lauf die produktive SaaS oder die produktive API angesprochen werden muss. Auch Fowler empfiehlt, externe Dienste lokal aufzusetzen, einen Fake zu platzieren oder eine dedizierte Testinstanz zu verwenden.

In der Praxis lässt sich eine Kombination aus

  • lokaler DB,
  • temporärem Verzeichnis,
  • Test-Host,
  • dedizierter Testumgebung, und
  • Fake-Service mit fixiertem Vertrag

gut handhaben.

7. Empfohlene Struktur für die Praxis

Für das Verhältnis gibt es keine absolut richtige Antwort. Ziemlich universell einsetzbar sind jedoch die folgenden drei Schichten.

Schicht Schwerpunkt Was hier steht
Kernschicht Unit-Tests dicht Geschäftsregeln, Zustandsübergänge, Eingabevalidierung, Fehlerklassifizierung
Randschicht Schmale Integrationstests DB, Dateien, HTTP, Serializer, DI, Konfiguration, COM, Berechtigungen
Gesamtschicht Wenige Smoke- / E2E-Tests Startprüfung, wichtigste Abläufe, Vorbeugung gegen wiederkehrende schwerwiegende Störungen

Gefühlt gilt: Der Unit-Test wird über die Anzahl dick, der Integrationstest über die Dichte an der Grenze.

Empfohlen wird folgendes Vorgehen:

  1. Zuerst die Grenzen der Anwendung auflisten
  2. Die Logik so gestalten, dass sie sich von der Außenwelt trennen lässt
  3. Für jede Grenze „mindestens einen Happy Path“ und „einen repräsentativen Failure Path“ platzieren
  4. Die Zahl der durchgängigen Gesamtläufe begrenzt halten
  5. Tritt ein Bug auf, den Test auf der Ebene ergänzen, auf der sich dieser Bug mit den geringsten Kosten reproduzieren lässt

Der letzte Punkt, 5, ist der wichtige.

  • Bei einem Regelfehler einen Unit-Test ergänzen
  • Bei einem Fehler in SQL / Binding / Konfiguration / Berechtigungen / Registrierung einen Integrationstest ergänzen
  • Bei einer Störung, die Start oder Auslieferung betrifft, einen Smoke- oder E2E-Test ergänzen

Wächst die Testmenge auf diese Weise, bleiben die Verantwortlichkeiten der Tests weniger anfällig für Vermischung.

8. Fünf Fragen für den Zweifelsfall

Zum Schluss fünf Fragen als Checkliste für den Zweifelsfall.

  1. Bleibt die zu prüfende Bedeutung erhalten, wenn Sie durch einen In-Memory-Fake ersetzen?
    • Wenn ja, tendiert es zum Unit-Test.
  2. Würden Sie im Fehlerfall eher die Verbindung oder Konfiguration verdächtigen als die Logik?
    • Wenn ja, tendiert es zum Integrationstest.
  3. Ist DB / Datei / Serializer / DI / Route / Model Binding / OS / Berechtigung / Bitness / Thread das eigentliche Thema?
    • Wenn ja, tendiert es zum Integrationstest.
  4. Wollen Sie eine große Zahl an Eingabemustern schnell durchlaufen lassen?
    • Wenn ja, tendiert es zum Unit-Test.
  5. Ist sofort klar, was zu reparieren ist, wenn dieser Test fehlschlägt?
    • Wenn nicht, sind die Testebenen vermischt.

Mit diesen fünf Fragen lässt sich die unscharfe Entscheidung „Integrationstest, weil es irgendwie näher am echten System ist“ oder „Unit-Test, weil es irgendwie schneller ist“ leichter vermeiden.

9. Zusammenfassung

Die Grenze zwischen Unit-Test und Integrationstest lässt sich am praktikabelsten nicht anhand des Codeorts, sondern anhand von welche Unsicherheit reduziert werden soll festlegen.

Die wesentlichen Punkte lassen sich auf diese fünf reduzieren:

  • Der Unit-Test ist ein Test der Entscheidung
  • Der Integrationstest ist ein Test der Verbindung
  • Die vollständige Abdeckung der Verzweigungen gehört in den Unit-Test
  • Format, Verdrahtung, Umgebung und Zeit gehören in den Integrationstest
  • Die durchgängige Gesamtprüfung wird mit wenigen Smoke- / E2E-Tests abgedeckt

Am meisten zu vermeiden sind diese drei Dinge:

  • sich mit einem Mock einbilden, auch die Verbindung zum echten Ding bewiesen zu haben
  • versuchen, im Integrationstest alle Verzweigungen abzudecken
  • die Verantwortlichkeiten von Unit-Test und Integrationstest vermischen

Im Zweifel prüfen Sie zuerst, ob der Fehler eine „Entscheidung“ oder eine „Verbindung“ betrifft. Mit dieser einen Frage lässt sich ein erheblicher Teil der Fälle klären.

10. Verwandte Artikel

11. Quellen

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Windows-App-Entwicklung

In Windows-Anwendungen wirken sich Grenzen wie Dateien, Berechtigungen, COM sowie 32-Bit / 64-Bit unmittelbar auf die Testebenen aus, weshalb sich dies gut mit der Klärung des Implementierungsansatzes verbindet.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Wie sollte man Unit-Tests und Integrationstests sinnvoll gegeneinander abgrenzen?
Kurz gesagt: Ein Unit-Test ist ein „Test der Entscheidung“, ein Integrationstest ein „Test der Verbindung“. Dinge, deren Bedeutung ohne externe Ressourcen vollständig ist — Preisberechnung, Zustandsübergänge, Eingabevalidierung, Klassifizierung von Ausnahmen —, gehören eher in Unit-Tests. Dinge, die „im Moment der Verbindung verraten, dass sie kaputt sind“ — SQL-Ausführung, JSON-/CSV-Serialisierung, Routing, DI-Registrierung, Dateisperren, Berechtigungen, COM-Registrierung, 32-Bit / 64-Bit —, gehören eher in Integrationstests. Lässt sich etwas mit beidem prüfen, beginnt man zuerst mit dem Unit-Test.
Was sollte in einen Integrationstest hochgestuft werden?
Das lässt sich grob in vier Grenzen einteilen: Format, Verdrahtung, Umgebung und Zeit. Format meint JSON/CSV oder das Mapping der Datenbank sowie Zeichenkodierungen, Verdrahtung meint die Verkabelung realer Bauteile wie DI-Registrierung, Routing und Modellbindung, Umgebung meint das tatsächliche Verhalten des Betriebssystems wie Dateiberechtigungen, COM-Registrierung, 32-Bit / 64-Bit und STA/MTA, und Zeit meint Timeout, Cancellation und Race Conditions. Bei diesen geht die Bedeutung mit einem In-Memory-Fake verloren, weshalb sie sicherer mit Integrationstests abgedeckt werden.
Was ist das Problem, wenn ein Unit-Test zu viele Mocks braucht?
Braucht ein Test 7 Mocks, ist das Setup lang, oder ist nicht mehr erkennbar, was eigentlich geprüft werden soll, dann hat die betreffende Klasse entweder zu viele Verantwortlichkeiten, oder es wurde eine Verdrahtung, die eigentlich in einen Integrationstest gehört, in den Unit-Test gedrückt. Ein Mock ist ein Werkzeug, um die Außenwelt abzuschneiden, nicht ein Werkzeug, um zu beweisen, dass die Verbindung zum echten Ding korrekt ist. Verwechselt man das, entsteht leicht die Situation, dass „alles grün ist, aber die Produktion trotzdem abstürzt“.
Mit welcher Struktur und welchem Verhältnis sollte man Tests führen?
Universell einsetzbar ist eine dreischichtige Struktur. Die Kernschicht hält Geschäftsregeln und Zustandsübergänge dick mit Unit-Tests ab, die Randschicht platziert schmale Integrationstests für DB, Dateien, Serializer und DI, und die Gesamtschicht deckt Startprüfungen und die wichtigsten Abläufe mit wenigen Smoke-/E2E-Tests ab. Die vollständige Abdeckung aller Verzweigungen läuft über Unit-Tests, Integrationstests werden je Grenze auf mindestens einen Happy Path und einen repräsentativen Failure Path beschränkt. Tritt ein Bug auf, fügt man den Test auf der Ebene hinzu, auf der sich dieser Bug mit den geringsten Kosten reproduzieren lässt.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog