Teststrategie und Testpyramide

Diese Seite beschreibt, was auf welcher Ebene getestet wird und mit welchem Werkzeug. Für die Methode (wie Tests entstehen), siehe TDD-Dokumentation.

Die Ebenen

Ebene Werkzeug Läuft unter Geschwindigkeit Docker nötig?
Domain-Unit-Tests JUnit 5, AssertJ mvn test Millisekunden Nein
Application-/Use-Case-Tests JUnit 5, AssertJ, Mockito mvn test Millisekunden Nein
Web-Slice-Tests @WebMvcTest, MockMvc mvn test ~1 Sekunde Nein
Architektur-Tests ArchUnit mvn test (Modul architecture-tests) Sekunden Nein
Integrationstests (*IT.java) @SpringBootTest, Testcontainers mvn verify (Failsafe) Sekunden bis Minuten Ja

Namenskonvention: *Test.java = schnelle Ebenen (Surefire, mvn test), *IT.java = Testcontainers-Integrationstests (Failsafe, erst mvn verify). So bleibt die tägliche TDD-Feedback-Schleife (mvn test) durchgehend im Sekundenbereich, auch wenn das Projekt wächst.

Domain-Unit-Tests

Testen Value Objects und Aggregate direkt, ohne jeden Spring-Kontext. Beispiel: MoneyTest, AggregateRootTest in common-domain. Das ist die Basis der Pyramide — die meisten Tests in diesem Projekt liegen hier, weil Geschäftsregeln (Geldbeträge, Kontostatus-Übergänge, Scoring-Regeln) bewusst in der Domain-Schicht konzentriert sind, nicht in Controllern oder Repository-Adaptern (siehe ADR-0002).

Application-/Use-Case-Tests

Testen eine Anwendungsfall-Klasse (z. B. OpenAccountUseCase) mit gemockten Ports (Mockito). Kein Spring-Kontext. Prüfen die Orchestrierung: welche Port-Methoden werden in welcher Reihenfolge mit welchen Argumenten aufgerufen, welche Domain-Fehler werden auf welche Anwendungsfall-Ergebnisse abgebildet.

Web-Slice-Tests

@WebMvcTest lädt nur die Web-Schicht (Controller, @ControllerAdvice, JSON-Serialisierung), mit gemocktem Anwendungsfall dahinter. Prüft HTTP-Status-Codes, Response-Bodies, Validierungsfehler-Mapping (GlobalExceptionHandler aus common-web) — ohne Datenbank oder vollen Anwendungskontext.

Architektur-Tests

Siehe architecture-tests und TDD für Architekten, Abschnitt "Architekturtests als ausführbare ADRs". Laufen unter mvn test (schnell genug, keine Docker-Abhängigkeit), da sie nur bereits kompilierte .class-Dateien analysieren.

Integrationstests

@SpringBootTest mit echtem PostgreSQL/Kafka via Testcontainers (siehe ADR-0006), Namensmuster *IT.java. Zwei Hauptkategorien:

Bekannter Stolperstein: Docker-Desktop-Kontention auf geteilten Entwicklungsmaschinen

Auf einer Maschine, auf der gleichzeitig viele andere Container laufen (CI-Agenten, weitere Lernprojekte, Observability-Stack), kann die Docker-Desktop-API kurzzeitig mit einer leeren Antwort auf /info reagieren, obwohl docker ps/docker info in der Kommandozeile normal funktionieren. Testcontainers meldet das als Could not find a valid Docker environment, obwohl Docker tatsächlich laeuft. In diesem Fall hilft meist ein einfacher Wiederholungslauf (mvn verify erneut ausführen) - kein Hinweis auf einen Fehler im eigenen Code. Ein echter Bug wurde auf dem Weg dorthin trotzdem gefunden: AuroraBankTestContainers.kafka() musste apache/kafka-native explizit per asCompatibleSubstituteFor(...) als Ersatz fuer confluentinc/cp-kafka deklarieren, sonst wirft der KafkaContainer-Konstruktor immer eine IllegalStateException (siehe zugehöriger fix:-Commit) - genau der Unterschied zwischen Infrastruktur-Flakiness (Retry hilft) und einem echten Defekt (Retry hilft NICHT), den man beim Lesen von Testcontainers-Fehlermeldungen auseinanderhalten muss.

Weiterer echter Bug, gefunden ueber einen Domain-Test: BigDecimal-Skalierung

Account.accrueInterest() in accounts-service berechnete 1000.00 * 0.02 = 20.0000 (Skalierung 4) und addierte das zu 1000.00 (Skalierung 2) - das Ergebnis 1020.0000 war laut BigDecimal-/Record-equals() ungleich 1020.00, obwohl fachlich derselbe Betrag gemeint war. Der Domain-Test accrueInterest_erhoehtSaldoBeiSparkontoUmDenZinssatz deckte das auf; behoben durch Normalisierung auf currency.getDefaultFractionDigits() im kanonischen Konstruktor von Money (siehe TDD-Katalog, common-domain-Abschnitt). Klassisches Beispiel dafuer, dass ein Domain-Test in einem Service (accounts-service) einen latenten Fehler im gemeinsam genutzten Kern (common-domain) aufdecken kann.

Bekannter Stolperstein: Mockito-Feldinitialisierer vs. @BeforeEach

In vier Anwendungsfall-Tests von lending-service (ScoreCreditApplicationUseCaseTest, GenerateRepaymentScheduleUseCaseTest, FinalizeLoanDecisionUseCaseTest, GetLoanApplicationUseCaseTest) wurde die zu testende Anwendungsfall-Klasse zunaechst als Feld-Initialisierer angelegt: private final XxxUseCase useCase = new XxxUseCase(repository, ...). Das Ergebnis: jeder Test schlug mit einer NullPointerException fehl, nicht mit seiner eigentlichen Assertion. Ursache: MockitoExtension injiziert @Mock-Felder erst in einem JUnit-5-Extension-Callback (postProcessTestInstance), der nach der Feldinitialisierung der Testinstanz laeuft - der Feld-Initialisierer erfasste also noch ein nicht-injiziertes (null) Repository. Behoben durch Konstruktion in @BeforeEach statt als Feld (siehe zugehöriger fix:-Commit im TDD-Katalog). Lehre: bei @ExtendWith(MockitoExtension.class) gehört jede Konstruktion, die gemockte Felder verwendet, in eine Lifecycle-Methode, nie in einen Feld-Initialisierer.

Bekannter Stolperstein: nackte new ObjectMapper() ohne registrierte Module

RegisterCustomerUseCaseTest und OutboxRelayTest (customer-service, Phase 7) instanziierten zunächst direkt new ObjectMapper(), um CustomerRegisteredEvent-Payloads zu (de-)serialisieren - beide Tests scheiterten mit InvalidDefinitionException: Java 8 date/time type java.time.Instant not supported by default. Der produktiv verwendete ObjectMapper-Bean wird von Spring Boot autokonfiguriert und registriert dabei automatisch alle auf dem Klassenpfad gefundenen Jackson-Module (u. a. jackson-datatype-jsr310 für Instant); eine "nackte" new ObjectMapper() im Test tut das nicht. Behoben durch new ObjectMapper().findAndRegisterModules() in beiden Testklassen - ein Beispiel dafür, dass ein Test-Double (hier: ein selbst instanziiertes Utility statt des echten Spring-Beans) sich unbemerkt vom Produktivverhalten unterscheiden kann.

Coverage

Jedes Modul erzeugt einen JaCoCo-Bericht (target/site/jacoco/index.html) über mvn test (siehe Parent-POM, jacoco-maven-plugin). Es gibt bewusst keine reactor-weite Coverage-Aggregation und keine harte CI-Schwelle (anders als im Bibliothek-Enterprise-Lab) — für den Umfang dieses Projekts genügt der Bericht pro Modul als Leseindikator, nicht als Torwächter-Metrik; hohe Coverage ist in einem konsequent testgetriebenen Projekt ohnehin eine Konsequenz der Methode, kein separates Ziel (siehe ADR-0001).

⌂ Cockpit