TDD für Entwickler
Zielgruppe: Entwickler:innen, die in diesem Projekt (oder generell per TDD) Code schreiben. Voraussetzung: Einführung in TDD.
Diese Seite ist die praktische Anleitung: Wie schreibst du konkret den ersten Test, wie gehst du vor, wenn du nicht direkt weißt, wie die Lösung aussieht, und wie hältst du Tests langfristig wartbar? Jedes Beispiel referenziert echten Code aus diesem Repository.
Baby Steps
Der häufigste Anfängerfehler ist, einen zu großen Test zu schreiben ("prüft die komplette Kontoeröffnung inklusive IBAN-Generierung, Kunden-Verifikationsprüfung und Event-Veröffentlichung in einem Test"). Kleinere Schritte sind fast immer schneller, weil jeder rote Zustand sofort lokalisiert, was fehlt.
Konkretes Beispiel aus common-domain/src/main/java/.../Money.java — der Money-Werttyp entstand
in vier separaten Schritten, jeder sein eigenes Commit-Paar ([RED]/[GREEN]):
Money.of(betrag, waehrung)erzeugt ein Objekt mit Getter-Zugriff.Money.oflehnt einen negativen Betrag ab.Money.add(other)addiert zwei Beträge gleicher Währung.Money.add(other)lehnt unterschiedliche Währungen ab.
Ein Entwickler, der sofort "Money mit voller Arithmetik, Validierung und Rundungsregeln" in einem Rutsch schreiben wollte, hätte wahrscheinlich länger gebraucht als diese vier kleinen, nachvollziehbaren Schritte zusammen — und hätte beim ersten Fehlschlag ein größeres Suchproblem.
Faustregel: Wenn du beim Schreiben eines Tests nicht in ein bis zwei Sätzen sagen kannst, was er prüft, ist er zu groß.
Triangulation
Manchmal ist die "offensichtliche" minimale Implementierung für einen einzelnen Test eine
Attrappe, die keine echte Logik enthält (z. B. return "APPROVED"; fest verdrahtet). Triangulation
bedeutet: einen zweiten Test mit anderen Eingabewerten hinzuzufügen, der die Attrappe zwingt,
durch echte Logik ersetzt zu werden, weil sonst beide Tests nicht gleichzeitig grün sein können.
Dieses Projekt nutzt Triangulation besonders intensiv im Scoring-Regelwerk von lending-service
(Phase 5) — dort gibt es viele Grenzfälle (Einkommensgrenzen, bestehende Schulden,
Risikoeinstufung), die sich kaum durch eine einzelne Formel erraten lassen, aber sehr gut durch
eine wachsende Tabelle konkreter Beispiele triangulieren lassen. Siehe
TDD-Katalog für den Verweis auf die konkrete Commit-Sequenz,
sobald diese Phase umgesetzt ist.
Transformation Priority Premise (TPP)
Bei der Frage "wie viel Produktionscode ist wirklich minimal, um von Rot nach Grün zu kommen"
hilft Robert C. Martins Transformation Priority Premise als Faustregel: bevorzuge einfachere
Transformationen (z. B. {} → {return konstante;} → return variable;) vor komplexeren (z. B.
das Hinzufügen einer Schleife oder Rekursion), solange der aktuelle Test das zulässt. Springt man
zu früh zu einer komplexen, generischen Lösung, verliert man den eigentlichen Vorteil kleiner
Schritte: dass jeder Zwischenschritt maximal einfach nachvollziehbar bleibt.
Given-When-Then / Arrange-Act-Assert
Alle Tests in diesem Projekt folgen intern derselben Dreiteilung, auch ohne ein BDD-Framework (kein Cucumber, um die Feedback-Schleife maximal schnell zu halten):
@Test
void withdraw_lehntAbhebungOhneDeckungAb() {
// Given (Arrange)
Account konto = AccountTestDataBuilder.aktivesKonto().mitGuthaben("50.00").build();
// When (Act)
ThrowableAssert.ThrowingCallable abheben = () -> konto.withdraw(Money.of("100.00", EUR));
// Then (Assert)
assertThatThrownBy(abheben)
.isInstanceOf(InsufficientFundsException.class);
}
Testmethodennamen sind auf Deutsch und beschreiben Verhalten, nicht Implementierung:
methode_erwartetesVerhaltenUnterBedingung (z. B. add_lehntUnterschiedlicheWaehrungenAb), nicht
testAdd2 oder shouldThrow.
Testdaten-Builder statt aufgeblähter Konstruktor-Aufrufe
Sobald ein Aggregate mehr als 2-3 Konstruktorparameter hat, werden rohe
new Account(id, ..., ..., ..., ...)-Aufrufe in Tests schnell unlesbar und bei jeder
Signaturänderung an zig Stellen kaputt. Dieses Projekt verwendet je Aggregate einen
Test-Data-Builder (Object-Mother-Variante) mit sinnvollen Standardwerten, z. B.
AccountTestDataBuilder.aktivesKonto().mitGuthaben("50.00").build() — nur die für den jeweiligen
Test relevante Abweichung wird explizit gesetzt, der Rest bleibt unsichtbares Rauschen.
Wie man mit einem TDD-Schritt beginnt, wenn die Lösung unklar ist
- Schreibe den einfachsten Testfall, den du dir vorstellen kannst — oft der "Glücksfall" (Happy Path) mit trivialen Werten.
- Lass ihn rot werden (Compile-Fehler zählt).
- Implementiere die dümmste Sache, die ihn grün macht (auch eine hartkodierte Konstante ist erlaubt, siehe Triangulation oben).
- Refactor, falls nötig.
- Wiederhole mit dem nächstkomplexeren Fall (Randwert, Fehlerfall, zweiter "echter" Wert).
Wenn du nach 2-3 Zyklen merkst, dass die Struktur nicht trägt (z. B. ein Value Object sollte eigentlich unveränderlich sein, ist es aber nicht) — refactore jetzt, nicht "später". Das grüne Sicherheitsnetz ist genau dafür da.
Wann ein Test ROT sein sollte, aber nicht kompilieren kann
Ein Test, der eine noch nicht existierende Klasse oder Methode referenziert, führt zu einem
Compile-Fehler, keinem Assertion-Fehler. Das zählt in diesem Projekt ausdrücklich als gültiger
"Red"-Zustand (siehe z. B. den allerersten Commit
test(common-domain): [RED] Money.of erzeugt Geldbetrag ... — zu diesem Zeitpunkt existierte die
Klasse Money noch gar nicht). Der Unterschied zu einem echten Assertion-Fehlschlag (z. B.
test(common-domain): [RED] Money.add lehnt unterschiedliche Waehrungen ab, wo Money bereits
existierte, aber die Prüfung noch fehlte) ist rein technisch — beide sind "Rot" im TDD-Sinn.
Häufige Fallstricke
- Zu viele Mocks — wenn ein Test fünf Kollaborateure mocken muss, ist das oft ein Hinweis auf zu viel Verantwortung in der getesteten Klasse (siehe TDD für Architekten, Abschnitt "Testbarkeit als Architekturtreiber").
- Tests, die Implementierungsdetails prüfen (z. B.
verify()auf eine private Hilfsmethode über Reflection) — sie brechen bei jedem Refactoring, obwohl sich das Verhalten nicht geändert hat. Teste über die öffentliche Schnittstelle. - "Ich schreibe den Test nachträglich" — ein nachträglicher Test bestätigt tendenziell das, was der Code zufällig tut, nicht das, was er tun soll. Siehe Einführung in TDD, Abschnitt "Warum vor dem Code testen".