Testing mit Testcontainers
Repository- und Outbox-Verhalten gegen echte PostgreSQL testen.
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.
@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.
@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")));
}
}
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.
@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.