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]):

  1. Money.of(betrag, waehrung) erzeugt ein Objekt mit Getter-Zugriff.
  2. Money.of lehnt einen negativen Betrag ab.
  3. Money.add(other) addiert zwei Beträge gleicher Währung.
  4. 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

  1. Schreibe den einfachsten Testfall, den du dir vorstellen kannst — oft der "Glücksfall" (Happy Path) mit trivialen Werten.
  2. Lass ihn rot werden (Compile-Fehler zählt).
  3. Implementiere die dümmste Sache, die ihn grün macht (auch eine hartkodierte Konstante ist erlaubt, siehe Triangulation oben).
  4. Refactor, falls nötig.
  5. 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

⌂ Cockpit