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)

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)

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:

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)

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.

⌂ Cockpit