Warum Unit-Tests an den Anfang Ihrer Pipeline gehören
Stellen Sie sich vor, Sie pushen am Freitagnachmittag eine Codeänderung. Der Build läuft durch, das Deployment wird ausgeführt, und Sie gehen nach Hause. Am Samstagmorgen leuchtet Ihr Telefon auf: Alarmmeldungen. Eine Rabattberechnung wendet negative Werte auf Kundenbestellungen an. Die Logik sah im Review korrekt aus. Aber niemand hat den Randfall entdeckt, bei dem ein Gutscheincode in Kombination mit einem Sonderpreis einen negativen Gesamtbetrag ergibt.
Diese Art von Problem sollen Unit-Tests abfangen. Nicht weil sie besonders ausgeklügelt sind, sondern weil sie schnell laufen, früh laufen und isoliert laufen. Sie sind die erste Verteidigungslinie in jeder Auslieferungspipeline.
Was Unit-Tets tatsächlich prüfen
Ein Unit-Test überprüft ein sinnvolles Verhalten ab dem Einstiegspunkt, an dem dieses Verhalten beginnt. In einem Backend-Dienst kann dieser Einstiegspunkt ein REST-Endpunkt oder ein Use Case sein. Die Anfrage darf durch die echten internen Schichten wandern: Controller, Service, Domänenlogik und Repository-Grenze. Der Test soll nicht beweisen, dass eine Methode eine andere Methode aufruft. Er soll beweisen, dass das System auf eine sinnvolle Eingabe korrekt reagiert.
Wenn Sie eine Funktion haben, die Versandkosten basierend auf Gewicht und Zielort berechnet, kann ein Unit-Test Folgendes bestätigen:
- Standardgewicht liefert Standardkosten
- Nullgewicht liefert Nullkosten
- Negatives Gewicht liefert einen Fehler oder Null
- Maximalgewicht liefert den Deckelwert
Was der Unit-Test nicht beweisen sollte, ist, ob die echte Datenbank diese Kosten korrekt speichert, ob das echte Zahlungsgateway sie akzeptiert oder ob ein benachbarter Dienst tatsächlich verfügbar ist. Das sind Aufgaben für andere Testarten.
Der Wert von Unit-Tests ist eng, aber tief. Sie geben Ihnen die Sicherheit, dass ein bestimmtes Verhalten noch funktioniert, wenn das umgebende System kontrolliert ist. Wenn Sie später Code ändern, sagen Ihnen bestandene Unit-Tests, dass Sie das bereits verifizierte Verhalten nicht kaputt gemacht haben.
Das Isolationsprinzip
Damit Unit-Tests schnell und zuverlässig sind, sollte die Außenwelt kontrolliert sein. Interne Anwendungsschichten sollten normal laufen. Externe Nachbarn sollten das Testergebnis nicht bestimmen. Das bedeutet in der Regel keine echte Produktionsdatenbankverbindung, kein Live-Drittanbieter-HTTP-Aufruf und keine Abhängigkeit von einem anderen laufenden Dienst. Wenn das Verhalten Daten benötigt, kann der Test kontrollierte Daten, eine In-Memory-/lokale Testdatenbank oder einen Fake, Mock oder Stub an der Systemgrenze verwenden.
Deshalb sollte Unit-Testing nicht als „ein Test pro Methode“ oder „ein Test pro Klasse“ definiert werden. Diese Definition ist zu mechanisch und führt Teams oft dazu, Implementierungsdetails zu testen. Ein Unit-Test wird besser als ein Verhaltenstest ab einem relevanten Einstiegspunkt verstanden, bei dem die Außenwelt ausreichend kontrolliert ist, sodass ein Fehler auf das getestete Verhalten zurückgeführt werden kann.
Mobiler Code ist ein nützliches Beispiel. Manches Verhalten ergibt nur innerhalb einer mobilen Laufzeitumgebung Sinn. In diesem Fall macht die Verwendung eines Emulators oder Simulators den Test nicht automatisch falsch. Die Frage ist, ob sich der Test immer noch auf ein Verhalten konzentriert und die Abhängigkeiten darum herum kontrolliert. Wenn ja, kann er in der Pipeline immer noch den Zweck eines Unit-Tests erfüllen.
Diese Isolation macht Unit-Tests schnell. Eine gut geschriebene Unit-Test-Suite für einen typischen Backend-Dienst ist in Sekunden fertig, nicht in Minuten. Vergleichen Sie das mit Integrationstests, die Container hochfahren oder eine Verbindung zu Testdatenbanken herstellen. Diese dauern Minuten.
Geschwindigkeit ist wichtig, weil schnelle Tests häufiger ausgeführt werden. Entwickler führen sie lokal aus, bevor sie Code pushen. CI-Pipelines führen sie sofort nach dem Build aus. Wenn etwas kaputt geht, wissen Sie es innerhalb von Sekunden oder Minuten, nicht nachdem Sie auf eine vollständige Testsuite gewartet haben, die eine halbe Stunde dauert.
Wo Unit-Tests in die Pipeline passen
Unit-Tests gehören in die früheste Phase Ihrer Pipeline, direkt nachdem der Code kompiliert oder gebaut wurde. Die Logik ist einfach: Wenn das grundlegende, von der Anwendung bereitgestellte Verhalten bereits kaputt ist, hat es keinen Sinn, langsamere Tests auszuführen, die von echten Nachbarsystemen abhängen.
Eine typische Reihenfolge der Pipeline-Stufen sieht so aus:
Hier ist ein praktisches Beispiel dafür, wie dieser erste Schritt in einer CI-Konfigurationsdatei aussieht:
# .gitlab-ci.yml oder ähnliche CI-Konfiguration
stages:
- build
- test
- deploy
build:
stage: build
script:
- npm install
- npm run build
test-unit:
stage: test
script:
- npm test -- --coverage
only:
- merge_requests
- main
- Code bauen oder kompilieren
- Unit-Tests ausführen
- Statische Analyse oder Linting durchführen
- Container-Images oder Artefakte bauen
- Integrationstests ausführen
- In die Staging-Umgebung deployen
- End-to-End- oder Akzeptanztests ausführen
- In die Produktion deployen
Wenn Unit-Tests in Schritt 2 fehlschlagen, stoppt die Pipeline. Es werden keine Container gebaut. Keine Staging-Umgebung wird belegt. Es wird keine Zeit damit verschwendet, auf Integrationstests zu warten, die ohnehin fehlschlagen würden, weil die zugrunde liegende Logik falsch ist.
Das ist die schnelle Feedback-Schleife. Je früher Sie einen Fehler finden, desto günstiger ist er zu beheben. Ein Fehler, der während des Unit-Testings gefunden wird, kostet Minuten zur Behebung. Ein Fehler, der in der Produktion gefunden wird, kostet Incident Response, Rollback-Prozeduren, Kundenkommunikation und möglicherweise Datenreparatur.
Wann Unit-Tests nicht ausreichen
Unit-Tests haben einen blinden Fleck: Sie können nicht überprüfen, ob Komponenten im realen System zusammenarbeiten. Wenn Ihr Checkout-Verhalten von einer externen Zahlungs-API abhängt, kann ein Unit-Test prüfen, wie sich Ihr Code verhält, wenn die Zahlungsabhängigkeit Erfolg, Fehler, Timeout oder fehlerhafte Daten zurückgibt. Er kann Ihnen nicht sagen, ob die echte Zahlungs-API Ihr Anfrageformat akzeptiert, die Authentifizierung handhabt oder die erwartete Antwortstruktur zurückgibt.
Dafür brauchen Sie Integrationstests. Aber Unit-Tests erfüllen hier trotzdem einen Zweck. Sie überprüfen, ob Ihre Codestruktur korrekt ist, ob Parameter in der richtigen Reihenfolge übergeben werden und ob die Fehlerbehandlung wie erwartet funktioniert. Sie können nur die echte Integrationsprüfung nicht ersetzen.
Eine weitere Einschränkung ist, dass Unit-Tests keine Konfigurationsprobleme, Umgebungsunterschiede oder Infrastrukturprobleme abfangen können. Eine Funktion, die in Unit-Tests einwandfrei funktioniert, kann in der Produktion fehlschlagen, weil die Produktionsdatenbank eine andere Sortierungseinstellung hat oder weil eine erforderliche Umgebungsvariable fehlt. Diese Probleme erfordern andere Testansätze.
Wie viel Unit-Testing ist genug
Die Antwort hängt vom Risiko ab. Wenn eine Funktion geschäftskritische Logik implementiert, bei der ein Fehler finanzielle Verluste, Datenkorruption oder Sicherheitsprobleme verursachen könnte, möchten Sie gründliche Unit-Tests, die Normalfälle, Randfälle, Fehlerfälle und Grenzbedingungen abdecken.
Wenn eine Funktion Daten einfach nur von einem Ort zum anderen transportiert, ohne sie zu transformieren, kann ein einzelner Unit-Test, der bestätigt, dass der Durchgriff korrekt funktioniert, ausreichend sein. Stunden damit zu verbringen, erschöpfende Tests für trivialen Code zu schreiben, ist keine gute Zeitnutzung.
Ein praktischer Ansatz ist das risikobasierte Testen. Identifizieren Sie, welche Teile Ihrer Codebasis das höchste Risiko tragen, wenn sie fehlschlagen. Konzentrieren Sie Ihre Unit-Testing-Bemühungen dort. Für Code mit geringem Risiko schreiben Sie gerade genug Tests, um offensichtliche Fehler abzufangen.
Praktische Checkliste für Unit-Tests in Ihrer Pipeline
- Unit-Tests laufen vor allen Integrations- oder End-to-End-Tests
- Unit-Tests sind für die gesamte Codebasis innerhalb weniger Minuten abgeschlossen
- Unit-Tests benötigen keine externen Dienste, Datenbanken oder Netzwerkzugriff
- Jeder Unit-Test überprüft ein sinnvolles Verhalten ab einem relevanten Einstiegspunkt, nicht eine Methode oder Klasse
- Tests sind deterministisch: Gleiche Eingabe führt immer zum gleichen Ergebnis
- Fehlgeschlagene Unit-Tests stoppen die Pipeline sofort
- Entwickler können dieselben Tests lokal ausführen, bevor sie pushen
Die konkrete Erkenntnis
Bei Unit-Tests geht es nicht darum, perfekte Abdeckungszahlen zu erreichen. Es geht darum, die häufigste Klasse von Fehlern so früh wie möglich zu finden, mit dem geringsten Zeit- und Infrastrukturaufwand. Setzen Sie sie an den Anfang Ihrer Pipeline, halten Sie sie schnell, halten Sie sie isoliert und konzentrieren Sie sie auf die Logik, die am wichtigsten ist. Alles andere baut auf diesem Fundament auf.