Integrationstests: Probleme erkennen, wenn Komponenten miteinander kommunizieren

Sie haben eine Funktion geschrieben, die korrekt aussieht. Der Unit-Test läuft durch. Die Logik ist sauber. Dann deployen Sie die Anwendung – und sie beginnt, Fehler zu werfen. Die Datenbankspalte, die Sie vorausgesetzt haben, wurde letzte Woche umbenannt. Die externe API, die Sie aufrufen, hat ihr Antwortformat geändert. Der Service, von dem Sie abhängen, erwartet jetzt einen anderen Header.

Das ist die Lücke, die Unit-Tests nicht schließen können. Eine Funktion kann isoliert betrachtet perfekt korrekt sein und trotzdem fehlschlagen, wenn sie mit einer anderen Komponente kommunizieren soll. Integrationstests existieren genau dafür, solche Probleme zu finden.

Was Integrationstests tatsächlich prüfen

Integrationstests verifizieren, dass zwei oder mehr Komponenten korrekt zusammenarbeiten. Die Komponenten können Ihre Anwendung und eine Datenbank sein, Ihr Service und eine externe API oder zwei interne Dienste innerhalb desselben Systems.

Die Fehler, die sie aufdecken, betreffen selten falsche Logik. Es geht um nicht übereinstimmende Annahmen:

Das folgende Sequenzdiagramm zeigt einen typischen Konflikt: Ein Service sendet ein Datum als String, aber die Datenbank erwartet einen Timestamp, was zu einem Fehler führt.

sequenceDiagram participant Service participant Database Service->>Database: INSERT INTO orders (date) VALUES ('2025-04-01') Database-->>Service: ERROR: column "date" is of type timestamp but expression is of type text Note over Service,Database: Nicht übereinstimmende Annahme: String vs. Timestamp
  • Ihr Code sendet ein Datum als String, aber die Datenbankspalte erwartet einen Timestamp.
  • Ihr Service ruft eine API mit einem Query-Parameter auf, aber die API hat diesen Parameter in den Request-Body verschoben.
  • Ihre Anwendung nimmt an, dass ein Feld immer vorhanden ist, aber der vorgelagerte Service fügt es nur unter bestimmten Bedingungen ein.

Das sind keine Fehler, die Sie durch reines Code-Studium finden. Sie treten nur auf, wenn Komponenten tatsächlich Daten austauschen.

Die Fragilitätsfalle

Integrationstests haben den Ruf, langsam und anfällig zu sein. Diesen Ruf haben sie sich verdient. Je mehr reale Komponenten Sie einbinden, desto wahrscheinlicher schlagen Ihre Tests aus Gründen fehl, die außerhalb Ihres Codes liegen. Ein Netzwerk-Timeout. Ein abhängiger Service, der gerade nicht erreichbar ist. Testdaten, die durch einen vorherigen Lauf korrupt wurden.

Wenn das häufig passiert, verlieren Teams das Vertrauen in die Testergebnisse. Sie beginnen, Integrationstests zu überspringen oder Fehler zu ignorieren. Die Tests werden zu Rauschen statt zu Signalen.

Die Lösung ist nicht, Integrationstests zu vermeiden. Die Lösung ist, selektiv zu sein, was Sie mit echten Abhängigkeiten testen.

Auswahl, was mit echten Abhängigkeiten getestet wird

Nicht jede Abhängigkeit muss in Ihren Integrationstests real sein. Die Faustregel ist einfach: Testen Sie nur dann mit einer echten Abhängigkeit, wenn diese schwer zu simulieren ist oder häufig Probleme in der Produktion verursacht.

Datenbanken sind in der Regel den Test mit einer echten Instanz wert. Abfrageverhalten, Constraints, Transaktionen und Locking sind schwer akkurat zu mocken. Ein Mock sagt Ihnen vielleicht, dass Ihre Abfrage syntaktisch korrekt ist, aber er sagt Ihnen nicht, dass Ihre Abfrage bei gleichzeitigem Zugriff einen Deadlock verursacht oder dass Ihre Migration einen Spaltentyp geändert hat, den Ihr Code immer noch als String behandelt.

Externe APIs von Drittanbietern sind in der Pipeline in der Regel nicht den Test mit echten Endpunkten wert. Verwenden Sie Test-Doubles, die typische Antworten aufzeichnen. Das Risiko von flaky Tests durch Netzwerkprobleme oder API-Rate-Limits überwiegt den Nutzen. Heben Sie sich die echte Integration für Staging- oder Produktionsverifikation auf.

Interne Services innerhalb Ihrer Organisation liegen dazwischen. Sie können mit echten Instanzen testen, wenn sich das Interface häufig ändert und die Kosten einer Nichtübereinstimmung hoch sind. Ansonsten liefern Vertragstests oft bessere Signale bei geringerer Fragilität.

Eine praktische Entscheidungshilfe: Fragen Sie sich: „Wenn diese Abhängigkeit ein Problem hat, würde ich das aus der Anwendungslogik erkennen oder nur daraus, wie sie verbunden ist?“ Wenn die Antwort „aus der Verbindung“ ist – Antwortformat, Header-Struktur, Parameter-Reihenfolge – dann ist es ein Kandidat für einen Integrationstest mit einer echten Abhängigkeit. Wenn die Antwort „aus der Anwendungslogik“ ist, reichen Unit-Tests oder Vertragstests.

Integrationstests schnell und zuverlässig halten

Sobald Sie entschieden haben, was mit echten Abhängigkeiten getestet wird, befolgen Sie diese Praktiken, um Ihre Integrationstests nützlich zu halten:

Testen Sie nur die Verbindung, nicht die Geschäftslogik. Wenn Sie bereits Unit-Tests für Ihre Geschäftsregeln haben, wiederholen Sie diese nicht in Integrationstests. Ein Integrationstest für eine Datenbankabfrage sollte verifizieren, dass die Abfrage gegen eine echte Datenbank erfolgreich läuft und die erwartete Struktur zurückgibt. Er sollte nicht jeden Randfall der Geschäftslogik prüfen, die diese Abfrage verwendet.

Setzen Sie die Umgebung vor jedem Test zurück. Wenn Sie eine Datenbank verwenden, erstellen Sie isolierte Testdaten und bereinigen Sie sie nach dem Test. Tests, die von Zuständen abhängen, die vorherige Tests hinterlassen haben, sind fragil und schwer zu debuggen. Verwenden Sie Datenbanktransaktionen, die nach jedem Test zurückgerollt werden, oder starten Sie für jeden Testlauf einen frischen Testcontainer.

Begrenzen Sie die Anzahl der Integrationstests. Sie müssen nicht jede Parameterkombination testen. Testen Sie einen Happy Path und einige realistische Fehlerszenarien. Das Ziel ist Vertrauen, dass die Verbindung funktioniert, nicht die Abdeckung jeder möglichen Eingabe.

Wo Integrationstests in Ihre Pipeline passen

Integrationstests stehen zwischen Unit-Tests und End-to-End-Tests. Sie sind teurer als Unit-Tests, aber schneller und fokussierter als End-to-End-Tests.

Eine typische Pipeline führt zuerst Unit-Tests aus. Wenn diese bestehen, führt die Pipeline Integrationstests durch. Wenn die Integrationstests bestehen, fährt die Pipeline mit dem Deployment auf Staging oder Produktion fort. End-to-End-Tests, falls vorhanden, laufen später oder in einer separaten Umgebung.

Der Zweck von Integrationstests ist nicht, einen Abdeckungsgrad zu erreichen. Der Zweck ist, Ihnen das Vertrauen zu geben, dass die Verbindungen zwischen Komponenten noch funktionieren, wenn sich Ihr Code ändert.

Praktische Checkliste

  • Entscheiden Sie für jede externe Abhängigkeit: Test mit echter Instanz, Test mit Test-Double oder Vertrauen auf Vertragstests.
  • Führen Sie Integrationstests in einer isolierten Umgebung aus, die auf einen bekannten Zustand zurückgesetzt werden kann.
  • Halten Sie Integrationstests fokussiert auf das Verbindungsverhalten, nicht auf die Geschäftslogik.
  • Begrenzen Sie Integrationstests auf einen Happy Path und einige realistische Fehlerszenarien.
  • Überwachen Sie die Testausführungszeit. Wenn Integrationstests länger dauern als Unit-Tests, haben Sie wahrscheinlich zu viele oder die falsche Art.

Das Fazit

Integrationstests beantworten eine Frage, die Unit-Tests nicht beantworten können: „Arbeiten diese Komponenten tatsächlich zusammen?“ Die Antwort zu kennen, ist wertvoll, bevor Sie deployen. Aber Integrationstests sind ein Werkzeug, kein Ziel. Seien Sie selektiv, was Sie mit echten Abhängigkeiten testen, halten Sie die Tests schnell und isoliert, und nutzen Sie sie, um Vertrauen in Ihre Deployments aufzubauen – nicht, um Abdeckungszahlen zu jagen.