Einführung in Test-Driven Development
Zielgruppe: alle. Dieser Text setzt keine Vorkenntnisse voraus und legt das Vokabular fest, auf dem die folgenden Seiten (für Entwickler, für Architekten) aufbauen.
Herkunft
Test-Driven Development (TDD) wurde von Kent Beck Ende der 1990er/Anfang der 2000er im Rahmen von Extreme Programming (XP) popularisiert (Test-Driven Development: By Example, 2002). Die Kernidee ist älter (die Praxis, Tests vor dem Code zu schreiben, findet sich schon in frühen NASA-Projekten), aber Beck hat sie in einen wiederholbaren, disziplinierten Zyklus gegossen, der bis heute Industriestandard in vielen Teams ist.
Der Zyklus: Red — Green — Refactor
┌───────────┐ ┌───────────┐ ┌───────────┐
│ RED │ ---> │ GREEN │ ---> │ REFACTOR │ ---> (zurueck zu RED)
│ Test, der │ │ Minimaler │ │ Struktur │
│ (noch) │ │ Code, der │ │ verbessern│
│ fehlschl. │ │ gruen wird│ │ Tests │
│ │ │ │ │ bleiben │
│ │ │ │ │ gruen │
└───────────┘ └───────────┘ └───────────┘
- Red — Schreibe einen neuen, kleinen Test für ein Verhalten, das noch nicht existiert. Führe ihn aus und beobachte, dass er fehlschlägt (Compile-Fehler oder Assertion-Fehler). Das ist kein Zwischenschritt zum Überspringen: Ein Test, den du nie rot gesehen hast, könnte aus falschen Gründen grün sein (z. B. weil er gar nichts prüft).
- Green — Schreibe die minimale Produktionsänderung, die den Test grün macht. Nicht mehr. Kein "während ich schon dabei bin"-Code für spätere Anforderungen (YAGNI).
- Refactor — Jetzt, mit einem grünen Sicherheitsnetz, verbessere die Struktur (Duplikation entfernen, Namen klären, Methode extrahieren) ohne das Verhalten zu ändern. Tests bleiben die ganze Zeit grün. Wenn ein Test rot wird, ist das kein Refactoring mehr, sondern eine Verhaltensänderung — dann gehört sie in einen eigenen Red-Green-Schritt.
In diesem Projekt ist dieser Dreiklang nicht nur eine Empfehlung, sondern sichtbar in der
Commit-Historie: Jeder Schritt ist sein eigener Commit, mit dem Präfix [RED], [GREEN] oder
refactor:. Siehe Commit-Historie als Lernpfad, um das
selbst nachzuvollziehen.
Warum vor dem Code testen (und nicht danach)?
Ein Test, der nach dem Code geschrieben wird, bestätigt tendenziell, was der Code bereits tut (inklusive seiner Bugs). Ein Test, der vor dem Code geschrieben wird, zwingt dazu, das gewünschte Verhalten aus Sicht eines Aufrufers zu formulieren, bevor eine Implementierung existiert, die diese Sicht verzerren könnte. Das hat zwei Effekte:
- Design-Effekt: Um einen Test vor der Implementierung schreiben zu können, muss die Schnittstelle (Methodenname, Parameter, Rückgabewert, geworfene Ausnahme) bereits benutzbar sein. TDD zwingt so dazu, APIs aus Konsumentensicht zu entwerfen statt aus Implementierungssicht. Mehr dazu in TDD für Architekten.
- Abdeckungs-Effekt: Jede Zeile Produktionscode in diesem Projekt existiert nur, weil ein vorher geschriebener, fehlschlagender Test sie verlangt hat. Tote, ungetestete Vorratscode-Pfade entstehen so von vornherein nicht.
Testarten und der Unterschied zwischen "Unit" und "Isoliert"
- Solitary (isolierte) Unit-Tests — testen genau eine Klasse, alle Kollaborateure sind Test-Doubles (siehe unten). Beispiel: ein Anwendungsfall-Test, der das Repository-Port-Interface mockt.
- Sociable (gesellige) Unit-Tests — testen mehrere echte, zusammenarbeitende Klassen (z. B.
ein Aggregate und seine Value Objects), aber ohne Infrastruktur (keine Datenbank, kein
Netzwerk). Die meisten Domain-Tests in diesem Projekt (z. B.
MoneyTest) sind sociable in dem Sinn, dass sie echte Kollaborateure verwenden, aber weiterhin ohne jeden Spring-Kontext laufen. - Integrationstests — schließen echte Infrastruktur ein (PostgreSQL, Kafka via Testcontainers).
Langsamer, daher bewusst von
mvn testgetrennt und erst untermvn verifyausgeführt (siehe Teststrategie).
Test-Doubles (Ersatzobjekte)
Ein häufiges Missverständnis ist, alle Ersatzobjekte "Mocks" zu nennen. Nach Gerard Meszaros (xUnit Test Patterns) unterscheidet man:
| Test Double | Verhalten | Beispiel in diesem Projekt |
|---|---|---|
| Dummy | Wird nur übergeben, nie benutzt (Signatur-Füller) | ein Platzhalter-Parameter, der von einer Methode ignoriert wird |
| Stub | Liefert fest einprogrammierte Antworten | ein CreditScoringPort-Stub, der immer "APPROVED" liefert |
| Fake | Echte, aber vereinfachte Implementierung | ein In-Memory-AccountRepository statt einer echten Datenbank |
| Spy | Zeichnet Aufrufe auf, zur späteren Prüfung | Prüfen, dass save() genau einmal mit den erwarteten Werten aufgerufen wurde |
| Mock | Wie Spy, aber mit vorab definierten Erwartungen, die selbst fehlschlagen können | Mockito verify(repository).save(any()) |
In diesem Projekt kommt in den Application-/Use-Case-Tests überwiegend Mockito (Mock/Stub) für Ports zum Einsatz; in den Domain-Tests werden bewusst keine Test-Doubles gebraucht, weil dort mit echten Value Objects gearbeitet wird — ein Hinweis darauf, dass die Domain-Schicht selbst keine "schwierigen" Abhängigkeiten hat (siehe TDD für Architekten).
Was wird NICHT per TDD getestet?
TDD treibt Verhalten. Nicht alles im Repository ist Verhalten:
- Reine Konfiguration (Spring-
@Configuration-Klassen ohne eigene Logik,application.yml) wird nicht testgetrieben entwickelt — es gibt kein sinnvolles "Verhalten", das ein Unit-Test vor der Implementierung einfordern könnte. Sie wird stattdessen durch Integrations-/Slice-Tests mitgeprüft (z. B.@SpringBootTeststartet erfolgreich). - Test-Infrastruktur selbst (z. B.
common-testing, Testcontainers-Fabrikmethoden) wird nicht getestet — Werkzeug für Tests ist kein fachliches Verhalten mit eigenen Tests. - Architektur-Fitness-Functions (ArchUnit-Regeln in
architecture-tests) sind ein Grenzfall: sie werden test-first formuliert, um eine Architekturentscheidung explizit zu machen, sind aber manchmal schon beim ersten Schreiben grün, weil die Regel bislang nicht verletzt wurde. Ihr Wert liegt in der Verhinderung künftiger Verstöße, nicht im Treiben neuen Codes — siehe TDD für Architekten, Abschnitt "Architekturtests als ausführbare ADRs".
Weiterführend
- TDD für Entwickler — die Praxis: Baby Steps, Triangulation, gute Testnamen, Testdaten-Builder.
- TDD für Architekten — die Wirkung auf Systemdesign und Testbarkeit als Architekturtreiber.
- Commit-Historie als Lernpfad — wie man dieses Repository als lebendiges TDD-Lehrbuch liest.
- TDD-Katalog pro Use-Case — Tabelle aller Use-Cases mit Verweis auf ihre Commit-Historie.
- TDD-Techniken-Katalog — konkrete Techniken (Fake It, Triangulation, Outside-In, ...) mit Fundstelle im Code, damit nicht immer derselbe Handgriff wiederholt wird.