Teststrategie
Zielgruppe: Tester:in. Deckt den Stand nach Phase 8 ab: Unit-/Architektur-/Integrationstests (Backend, seit Phase 1), Frontend-Build-Verifikation (seit Phase 6), E2E via Playwright und Lasttest via k6 (beide seit Phase 8), Consumer-Driven Contract Testing (Pact-JVM) und reactor-weite Coverage-Schwelle (beide zusätzlich, nach Phase 8).
Test-Pyramide in diesem Projekt
/\
/E2E\ wenige - Playwright gegen Angular + echte Backend-Services (Phase 8)
/------\
/Integr. \ mehr - Testcontainers (Postgres/Kafka/RabbitMQ), pro Service (ab Phase 2)
/----------\
/ Unit-Tests \ die meisten - reine JUnit-Tests ohne Spring-Kontext, v. a. Domainmodell
/--------------\
Architektur-Tests quer zu allen Ebenen - ArchUnit, prüft Strukturregeln statt Verhalten
Bereits etabliert (Phase 1)
- Unit-Tests ohne Spring-Kontext für
common-domain(IsbnTest,MoneyTest,EmailAddressTest) - laufen in Millisekunden, keine Container nötig. Möglich, weilcommon-domainframework-frei bleibt (siehe Coding Guidelines). - Architektur-Tests (
architecture-tests, ArchUnit) - prüfen Strukturregeln (Modulgrenzen, Injection-Stil) bei jedemmvn verifyautomatisch. - Testcontainers-Fabrikmethoden (
common-testing.LibraryTestContainers) - bereitgestellt, aber noch ungenutzt (kein Service hat bisher eine Datenbank). Erste Verwendung ab Phase 2.
Zwei Wege, Testcontainers mit Spring Boot zu verbinden
Wird ab Phase 2 an echtem Code gezeigt, hier schon die Gegenüberstellung:
Modern (@ServiceConnection, Spring Boot 3.1+, in diesem Projekt der Standard):
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = LibraryTestContainers.postgres("catalog_db");
Spring erkennt den Container-Typ automatisch und verdrahtet spring.datasource.* selbst.
Klassisch (@DynamicPropertySource, vor Spring Boot 3.1 der einzige Weg):
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
Wird in älteren Projekten/Tutorials noch häufig gesehen - gut zu kennen, aber in diesem Projekt nicht der Standardweg.
Stolperstein: @WebMvcTest sieht eigene @AutoConfiguration-Module nicht
Beim Schreiben von MemberControllerTest (Phase 2) ist genau das passiert: @WebMvcTest lädt
nur eine von Spring Boot selbst kuratierte, feste Liste an Auto-Configurations (u. a.
Spring Securitys eingebaute OAuth2ResourceServerAutoConfiguration) - eigene
@AutoConfiguration-Klassen aus anderen Modulen (hier: CommonSecurityAutoConfiguration aus
common-security) werden nicht automatisch gefunden, obwohl sie korrekt über
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports registriert
sind (das funktioniert nur im VOLLEN Anwendungskontext, z. B. @SpringBootTest oder der echte
laufende Service).
Tückisch daran: es gibt keinen Fehler. Spring Boot fällt still auf seine eigene,
generische SecurityFilterChain zurück (nur "authentifiziert ja/nein", ohne unsere
Rollen-Autorisierung), und @PreAuthorize-Annotationen werden einfach nie ausgewertet - ein
Test, der eigentlich 403 erwartet, bekommt unbemerkt 200.
Lösung: die benötigten Auto-Configurations explizit in den Test holen:
@WebMvcTest(MemberController.class)
@ImportAutoConfiguration({CommonSecurityAutoConfiguration.class, CommonWebAutoConfiguration.class})
class MemberControllerTest { ... }
Lehre für Reviewer:innen (siehe auch PR-Checkliste): Ein
@WebMvcTest, der sicherheitsrelevantes Verhalten prüft, MUSS mindestens einen Testfall haben,
der eine falsche/fehlende Rolle mit 403 erwartet - ein Test, der nur den Erfolgsfall (200)
prüft, hätte dieses Problem nie aufgedeckt.
Frontend-Tests (library-frontend, ab Phase 6)
- Karma/Jasmine ist als Standard-Testrunner von Angular CLI vorkonfiguriert (
ng test). Ausgeführte Komponenten-Specs:src/app/app.component.spec.tsprüft die Shell-Komponente (Toolbar-Marke, Umschaltung Login-Landingpage ⟷<router-outlet>, rollenabhängige Admin-Links,login()/logout()-Aufrufe) mit einemFakeAuthServiceals Test-Double - der echteAuthServicewürde im Unit-Test ein Keycloak-Discovery-Dokument laden wollen. Weitere Feature-Specs sind eine sinnvolle Erweiterung (siehe Frontend-Lernpfad, Etappe 8).cd library-frontend npx ng test --watch=false --browsers=ChromeHeadless # -> TOTAL: 8 SUCCESS - Voraussetzung:
ng testbraucht einen installierten Chrome/Chromium für denChromeHeadless-Launcher. Ist auf der Maschine nur ein Chrome an einem ungewöhnlichen Pfad vorhanden, setzt manCHROME_BINauf diechrome.exe. Alternativen:karma.conf.jsum einenChromeHeadlessNoSandbox-Custom-Launcher mitpuppeteers gebündeltem Chromium ergänzen. - Stattdessen durchgeführte Verifikation (Äquivalent zu
mvn verifyauf Backend-Seite):ng buildundng build --configuration production- beide beinhalten den vollen Angular-AOT-Compiler inkl. Template-Typprüfung (strictTemplates). Das hat in der Praxis bereits zwei echte Fehler vor dem Commit gefangen: ein fehlenderDatePipe-Import in einer standalone Komponente (Pipes müssen dort explizit importiert werden, es gibt kein globalesCommonModule) und ein unzulässigesas-Alias auf einem@else if-Block (Angular-Control-Flow erlaubt; as xnur am primären@if).
E2E-Tests (Playwright, seit Phase 8)
Ein Referenzszenario in library-frontend/e2e/golden-path.spec.ts:
Login → Mitglied anlegen → Buch anlegen → im Katalog finden → ausleihen → in "Meine Ausleihen"
sichtbar. Übt damit Signals-Features, das NgRx-Feature (catalog-admin) und den
Keycloak-PKCE-Login in einem Durchlauf zusammen. Läuft gegen den ECHTEN Stack, kein Mocking.
e2e/login.ts fährt dabei den echten Keycloak-Authorization-Code-Flow mit PKCE (Formular auf der
Keycloak-Seite ausfüllen, zurück nach localhost:4200), playwright.config.ts setzt das Timeout
bewusst auf 90 s wegen der langsamen Erst-Requests frisch gestarteter Spring-Boot-Services.
Runbook: E2E lokal ausführen
Der Test braucht drei Dinge gleichzeitig laufend: Infrastruktur, Backend-Services, Angular-Dev-Server. Reihenfolge:
# 1. Infrastruktur + alle 9 Spring-Boot-Services (baut beim ersten Mal 9 Container-Images,
# dauert je nach Maschine 10-40 min):
cd infra
podman machine start # Windows/macOS - Linux uebersprungen
podman compose up -d
podman compose ps # warten bis alle "healthy" / "running" sind
podman compose logs -f api-gateway # bis "Started ...Application" erscheint
# 2. In einem zweiten Terminal: Angular-Dev-Server (muss auf :4200 laufen):
cd library-frontend
npx ng serve
# 3. In einem dritten Terminal: die Tests
cd library-frontend
npx playwright install chromium # einmalig
npm run e2e # headless -> erwartetes Ergebnis: "1 passed"
npm run e2e:ui # alternativ: interaktiver Modus zum Debuggen
Minimal nötige Container für genau dieses Szenario (falls die Maschine nicht alle 23 gleichzeitig
trägt): postgres, redis, kafka, keycloak, config-server, discovery-server,
catalog-service, member-service, lending-service, api-gateway. reservation-/fine-/
notification-service sowie die Observability-Container (Prometheus/Grafana/Zipkin/Loki) sind
für den Golden Path nicht nötig.
Voraussetzung Container-Laufzeit: Dieses Projekt nutzt Podman. podman compose (in
Podman 4.1+ eingebaut) liest die unveränderte infra/docker-compose.yml; alternativ das
separate podman-compose (pip install podman-compose, dann
podman-compose -f infra/docker-compose.yml up -d). Beachten:
- Unter Windows/macOS zuerst
podman machine start. - Das externe Netz
proxy(ausshared/enterprise-infrastructure/) muss existieren:podman network create proxy. api-gatewayhat eindepends_onauf alle sechs Fachservices - läuft man nur das Minimal- Subset,api-gatewaymit--no-depsstarten.discovery-/config-serverzuerst hochziehen, dann die Fachservices.- Für die Testcontainers-
*IT-Tests siehe docker-compose-guide.md, "Testcontainers mit Podman" (~/.testcontainers.propertiesausinfra/testcontainers/testcontainers.properties.example, Ryuk deaktiviert).
Schnell-Variante ohne In-Container-Maven-Build (infra/Dockerfile.prebuilt-jar +
infra/docker-compose.e2e.yml): den Reactor einmal auf dem Host bauen
(cd library-platform && mvn package -DskipTests), dann bauen die Service-Images nur noch das
fertige *-exec.jar in ein JRE-Image - Faktor ~5 schneller als neun getrennte Maven-Builds im
Container. Details im Kopfkommentar beider Dateien.
Status in diesem Repository
✅ Grün verifiziert - zuletzt nach dem Angular-18→22-Update (in zwei Etappen 18→20, 20→22,
jeweils Golden Path 1 passed gegen den lokal hochgefahrenen Stack: Podman-Machine, Minimal-Subset
via infra/docker-compose.e2e.yml, ng serve auf der jeweiligen Angular-Version, npm run e2e).
Der erste vollständige Live-Lauf (Phase 8) hat mehrere reale, zuvor unentdeckte Bugs aufgedeckt, die
kein Unit-/Integrationstest gefangen hatte - siehe
library-frontend/e2e/README.md für die
vollständige Liste. Das ist der eigentliche Wert von E2E-Tests: sie prüfen den tatsächlich
zusammengesetzten, laufenden Stack statt einzelner, isoliert gemockter Schichten.
Lasttest (k6, seit Phase 8)
infra/k6/borrow-load-test.js fährt eine Lastspitze
(bis 50 virtuelle Nutzer:innen) gegen POST /api/loans, um das Verhalten der
Resilience4j-Circuit-Breaker-/Retry-Konfiguration in lending-service unter Last sichtbar zu
machen. Legt sich seine Testdaten in einer setup()-Phase selbst an. Syntax mit
k6 run --vus 1 --iterations 1 gegen einen nicht laufenden Stack geprüft (schlägt dort
erwartungsgemäß nur an der HTTP-Verbindung fehl, nicht am Skript selbst) - Details und
Interpretation der Ergebnisse in infra/README.md.
Contract Testing
Seit lending-service echte Service-zu-Service-Aufrufe hat (Phase 3, CatalogFeignClient/
MemberFeignClient) - genau der Zeitpunkt, den dieses Kapitel bis dahin dafür vorgemerkt hatte -
prüft Pact-JVM alle 8 echten Producer/Consumer-Paare (2 REST, 6 Kafka-Events): Konsumententests
beschreiben den erwarteten Vertrag und erzeugen daraus eine Pact-Datei, Providertests verifizieren
die tatsächliche Anwendung dagegen. Siehe Pattern-Katalog
für die Fundstelle und Glossar für die Begriffsdefinition.
Datei-basierter Austausch statt Pact Broker. Die generierten Pact-JSON-Dateien liegen
eingecheckt unter contracts/pacts/ -
Git ist hier bewusst der "Broker" statt eines eigenen Pact-Broker-Servers (kein zusätzlicher
Infrastruktur-Container für dieses Lernprojekt). Ändert sich ein Contract absichtlich, gehören
Code-Änderung und aktualisierte Pact-Datei in denselben Commit.
Stolperstein: Reactor-Build-Reihenfolge vs. Pact-Erzeugungsreihenfolge. catalog-service und
member-service bauen im Maven-Reactor VOR lending-service (Modul-Reihenfolge in der Root-pom.xml)
- Provider-Verifikation dürfte in einem frischen Checkout also nicht davon abhängen, dass eine
Pact-Datei im SELBEN Build-Lauf frisch vom Konsumenten erzeugt wurde. Der datei-basierte Ansatz
löst das nebenbei: Konsumententests schreiben lokal direkt in den eingecheckten Ordner
(
pact.rootDir-Systemproperty, zentral in der Root-pom.xmlfür alle Module gesetzt), Provider-Verifikation liest von dort - unabhängig von der Build-Reihenfolge im selben Lauf.
REST vs. Kafka-Message-Pacts. REST-Providertests (*ProviderContractIT.java) brauchen einen
laufenden Spring-Kontext (@SpringBootTest(webEnvironment = RANDOM_PORT)) und einen
JwtDecoder-Test-Stub statt echtem Keycloak - Kafka-Message-Providertests dagegen brauchen KEINEN
Spring-Kontext (MessageTestTarget statt HttpTestTarget, @PactVerifyProvider-Methode gibt
direkt die erwartete Nachricht zurück) und laufen deshalb spürbar schneller.
Coverage
coverage-report (letztes Modul im Reactor) fasst die pro Modul bereits laufenden JaCoCo-Läufe zu
einer reactor-weiten Line-Coverage-Zahl zusammen (jacoco:report-aggregate) und lässt
mvn verify fehlschlagen, wenn diese unter eine Mindestschwelle fällt (jacoco:check, siehe
coverage-report/README.md für die
Begründung des Aufbaus inkl. eines JaCoCo-Stolpersteins bei reactor-weiten Checks). Report als
CI-Artefakt unter coverage-report/target/site/jacoco-aggregate/index.html.
Schwellenwert 30 % - ein gemessener, kein geratener Startwert. Ohne laufende Podman-Machine
lässt sich lokal nur mvn test (Unit-Tests, keine Container) ausführen; das ergab 33 %
reactor-weite Line-Coverage. Die zahlreichen Testcontainers-*IT-Tests (Repository-Adapter,
Kafka-Consumer, Provider-Contract-Verifikation) zählen darin NICHT mit - sie laufen erst mit
laufendem Podman-Socket (lokal, siehe
Testcontainers mit Podman)
bzw. in der CI (Linux-Runner mit rootless Podman), wo die tatsächliche Zahl entsprechend höher
ausfallen dürfte. 30 % statt 33 % als kleiner Puffer gegen Schwankungen. Sobald ein CI-Lauf mit
vollständigem mvn verify eine reale, höhere Zahl liefert, gehört die Schwelle in
coverage-report/pom.xml angehoben - dokumentiert als offene Nachjustierung, kein vergessener
Punkt.