Lab 09

Testing mit Testcontainers

Repository- und Outbox-Verhalten gegen echte PostgreSQL testen.

TestcontainersPostgreSQLIntegrationstestFlyway
SchwierigkeitFortgeschritten
Dauer90–120 Min
LernstufeStufe 6 – Praxislabor und Capstone
Arbeitsweise: Erst den Vorher-Code öffnen, dann Analyse lesen, anschließend die Nachher-Lösung und den Test vergleichen. Alle Abschnitte sind standardmäßig geschlossen.

Workshop-Slice

Dieses Lab zeigt einen konkreten Enterprise-Fehler mit Vorher/Nachher-Code. Die Beispiele sind bewusst fachlich benannt, damit du Architekturentscheidungen und nicht nur Syntax übst.

Lernziel

Du ersetzt zu schwache Mock-Tests durch einen echten Integrationsnachweis gegen Datenbank und Migrationen.

Ausgangslage

Das Repository wird nur gemockt. Fehler in SQL, Constraints, JSONB-Spalten oder Flyway-Migrationen fallen erst in der Umgebung auf.

Vorher: problematischer Code

Der Test beweist nur, dass ein Mock aufgerufen wurde, nicht dass Persistenz funktioniert.

OrderServiceMockTest.javaJAVA
@Test
void savesOrder() {
    var repository = mock(OrderRepository.class);
    var service = new OrderService(repository);

    service.create("O-88");

    verify(repository).save(any());
}
Analyse: Was ist daran schlecht?
  • Keine echte Tabelle, kein Constraint, keine Transaktion.
  • Flyway-/Liquibase-Migrationen werden nicht geprüft.
  • JSONB/Index/Unique-Fehler bleiben unentdeckt.
  • Test gibt falsche Sicherheit.
Nachher: bessere Lösung

Die Zielversion startet PostgreSQL im Test und verwendet dieselben Migrationen wie lokal/CI.

OrderRepositoryIT.javaJAVA
@Testcontainers
@SpringBootTest
class OrderRepositoryIT {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @DynamicPropertySource
    static void database(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
        registry.add("spring.flyway.enabled", () -> true);
    }

    @Autowired OrderJpaRepository repository;

    @Test
    void enforcesUniqueBusinessId() {
        repository.save(new OrderEntity("O-88"));
        assertThrows(DataIntegrityViolationException.class,
            () -> repository.saveAndFlush(new OrderEntity("O-88")));
    }
}
V003__outbox.sqlSQL
create table outbox_event (
  event_id uuid primary key,
  aggregate_id varchar(80) not null,
  payload jsonb not null,
  published_at timestamp null
);
Test / Prüfnachweis

Dieser Abschnitt zeigt, wie du die Verbesserung nachweist. Es ist bewusst kein reiner Happy-Path-Test, sondern prüft ein Risiko aus dem Vorher-Teil.

OutboxRepositoryIT.javaJAVA
@Test
void storesJsonPayloadAndFindsUnpublishedEvents() {
    repository.store(new OutboxEvent(UUID.randomUUID(), "O-88", "{\"type\":\"OrderCreated\"}"));

    assertThat(repository.findUnpublished())
        .hasSize(1)
        .first()
        .extracting(OutboxEvent::aggregateId)
        .isEqualTo("O-88");
}
Typische Fehler
  • Nur H2 statt PostgreSQL testen, obwohl Produktion PostgreSQL nutzt.
  • Container pro Testmethode neu starten.
  • Migrationen im Test deaktivieren.
  • Integrationstests ohne klare Namenskonvention mit Unit-Tests mischen.
Deep-Learning-Bezug

Die Links führen zum ausführlichen Inhalt; die Deep-Learning-Seite bleibt nur die Lernlandkarte und kopiert den Inhalt nicht doppelt.

Prüfcheckliste
  • Test startet echte PostgreSQL-Instanz.
  • Migrationen laufen im Test.
  • Mindestens ein Constraint wird wirklich geprüft.
  • Test ist als IT erkennbar und CI-fähig.
⌂ Cockpit