TDD für Architekten
Zielgruppe: Architekt:innen und technische Leads, die Systemstruktur, Schnittstellen und Qualitätsziele verantworten. Voraussetzung: Einführung in TDD.
TDD wird oft als reine Entwicklerpraxis dargestellt ("schreib den Test zuerst"). Auf Architekturebene ist die interessantere Frage: welche Struktur muss ein System haben, damit TDD überhaupt praktikabel ist — und was verrät es über ein bestehendes Design, wenn TDD sich schwer anfühlt?
Testability als Architekturtreiber
Ein Test, der vor der Implementierung entsteht, kann sich nicht an eine bestehende Klassenstruktur "anschmiegen" — er zwingt dazu, die Schnittstelle so zu entwerfen, dass sie isoliert aufrufbar ist. Das hat in diesem Projekt konkrete strukturelle Konsequenzen:
- Konstruktor-Injection statt Field-Injection (
@Autowiredauf Feldern) — ein Objekt, das seine Abhängigkeiten nur per Konstruktor bekommt, lässt sich in einem Unit-Test direkt mitnew Foo(mockA, mockB)erzeugen. Field-Injection erzwingt einen Spring-Kontext, selbst für einen trivialen Test — das verlangsamt die TDD-Feedback-Schleife drastisch. - Ports (Interfaces) statt konkreter Infrastruktur-Klassen in der Anwendungsschicht — ein
Anwendungsfall wie "Konto eröffnen" hängt von einem
AccountRepository-Interface ab, nicht vonJpaAccountRepository. Ein Anwendungsfall-Test kann das Interface mocken und läuft in Millisekunden, ganz ohne Datenbank. Die konkrete JPA-Implementierung wird separat, mit Testcontainers, gegen eine echte PostgreSQL getestet. - Framework-freier Domain-Kern (
common-domain, sieheCommonDomainPurityTest) — Value Objects und Aggregates ohne Spring-/Jakarta-Abhängigkeit lassen sich ohne jeden Anwendungsstart testen. Das ist der schnellste Layer der Testpyramide (siehe Teststrategie) und treibt indirekt die hexagonale/Ports-und-Adapter-Struktur jedes Service, weil Infrastruktur zwangsläufig hinter einer Domain-definierten Schnittstelle landen muss, damit die Domain testbar bleibt.
Der Kausalpfeil zeigt in diese Richtung: nicht "wir haben Hexagonal Architecture gewählt und schreiben deshalb Tests", sondern "wir wollen jeden Anwendungsfall in Millisekunden isoliert testen können, und das erzwingt fast zwangsläufig Ports-und-Adapter" (siehe ADR-0002).
Ein schwerer Test ist ein Architektur-Symptom, kein Test-Problem
Wenn ein Test für einen Anwendungsfall fünf Mocks braucht, drei Setup-Zeilen für unrelated Daten enthält oder eine tiefe Objektgraph-Konstruktion erfordert, ist die naheliegende Reaktion "der Test ist zu kompliziert, ich schreibe weniger Tests". Die produktivere Lesart: die getestete Klasse hat zu viele Verantwortlichkeiten oder zu viele Abhängigkeiten (Verletzung des Single-Responsibility-Prinzips). TDD macht dieses Symptom sichtbar, bevor der Code in Produktion ist — ein nachträglich geschriebener Test hätte denselben Befund erst nach dem Fakt geliefert, wenn eine Umstrukturierung deutlich teurer ist.
Outside-In vs. Inside-Out TDD
Zwei verbreitete Strategien, in welcher Reihenfolge Schichten getrieben werden:
- Outside-In (London-Schule) — beginne am äußersten Rand (z. B. einem REST-Controller-Test
oder einem Akzeptanztest für den ganzen Use-Case), mocke zunächst alles darunter, und "wachse"
die Implementierung nach innen, Schicht für Schicht. Vorteil: von Anfang an klar, wie sich der
Use-Case für einen Konsumenten anfühlt. In diesem Projekt der bevorzugte Ansatz für
Use-Cases mit REST-Endpoint (z. B.
POST /accounts). - Inside-Out (Chicago/Classic-Schule) — beginne im Kern (z. B. dem
Money-Value-Object oder einer Aggregate-Invariante) und baue nach außen. Vorteil: die fachlichen Regeln stehen zuerst fest, unabhängig davon, wie sie später aufgerufen werden. In diesem Projekt der Ansatz fürcommon-domainselbst (siehe die Money-Zyklen) und für regelintensive Kernlogik wie das Kredit-Scoring inlending-service.
Dieses Projekt mischt beide bewusst je nach Use-Case-Charakter — dokumentiert jeweils im TDD-Katalog.
Testpyramide im Microservice-Zuschnitt
▲
/E2E\ wenige, langsam, hohe Aussagekraft ueber den ganzen Fluss
/-----\
/Contract\ pro Service-Paar (Event-Schema-Kompatibilitaet)
/---------\
/Integration\ Testcontainers: Persistenz, Kafka-Konsumenten
/-------------\
/ Application/ \ Use-Case-Tests, Ports gemockt
/ Web-Slice \
/-------------------\
/ Domain-Unit-Tests \ keine Spring-Context, Millisekunden, die meisten Tests
/-------------------------\
Details und Werkzeuge pro Ebene: siehe Teststrategie. Aus
Architektursicht ist die Kernaussage: die Basis muss die breiteste sein. Ein System, in dem
die meisten Tests @SpringBootTest-Integrationstests sind, hat in der Regel eine Domain, die zu
eng mit Infrastruktur verwoben ist, um isoliert testbar zu sein — ein Architektur-Befund, keine
reine Test-Stil-Frage.
Bezug zu ATDD/BDD
Dieses Projekt verwendet kein separates BDD-Werkzeug (kein Cucumber/Gherkin) — die deutschen, verhaltensbeschreibenden Testmethodennamen (siehe TDD für Entwickler) übernehmen dieselbe Funktion: lesbare Spezifikation direkt im Code, ohne zusätzliche Übersetzungsebene. Acceptance-Test-Driven Development (ATDD) — Abnahmekriterien eines Use-Case als ausführbaren Test formulieren, bevor die Implementierung beginnt — findet in diesem Projekt auf Ebene der Web-Slice-/Application-Tests statt: der erste Test eines neuen Use-Case ist typischerweise bereits eine grobe Abnahmebedingung ("Konto mit gültigen Daten eröffnen liefert 201 und eine IBAN").
Architekturtests als ausführbare ADRs
Die ArchUnit-Regeln in architecture-tests (z. B. CommonDomainPurityTest) sind eine
Sonderform von TDD: Sie werden test-first geschrieben, um eine Architekturentscheidung explizit
und dauerhaft durchsetzbar zu machen — sind aber oft schon beim ersten Ausführen grün, weil die
Regel bislang nicht verletzt wurde (anders als ein normaler Produktionscode-Test, der vor der
Implementierung zwangsläufig rot ist). Ihr Wert zeigt sich später: Wenn ein Refactoring-Schritt
(dritte Phase des TDD-Zyklus!) versehentlich eine Schichtgrenze verletzt, schlägt die
Architekturregel fehl — noch bevor ein Reviewer es manuell bemerken müsste. Man kann sie daher als
ausführbare, versionierte ADRs lesen: Die zugehörige ADR erklärt das Warum, die ArchUnit-Regel
erzwingt das Dass.
Wann NICHT strikt TDD?
TDD ist kein Dogma für jede Codezeile. Fälle, in denen dieses Projekt bewusst davon abweicht (siehe auch Einführung in TDD, Abschnitt "Was wird NICHT per TDD getestet"):
- Explorative Spikes — wenn die Lösung technisch unklar ist (z. B. "funktioniert dieses Kafka-Konfigurationsmuster überhaupt wie erwartet?"), ist ein Wegwerf-Prototyp ohne Tests legitim, um schnell zu lernen. Der Prototyp wird danach verworfen und die eigentliche Implementierung beginnt sauber per TDD.
- Reine Konfiguration und Wiring — siehe Einführung.
- UI-Layout-Feinschliff (in diesem Projekt nicht relevant, da bewusst kein Frontend enthalten ist) — visuelle Details lassen sich schlecht sinnvoll vor-testen.
Architektonisch wichtig ist, diese Ausnahmen bewusst und benannt zu halten, statt TDD schleichend "wenn's grad passt" aufzugeben — sonst verliert das Sicherheitsnetz seinen Wert genau dort, wo Komplexität am größten ist.