The full picture
@SpringBootTest
@Testcontainers
class OrderRepositoryIT {
@Container @ServiceConnection // Boot wires spring.datasource.*
static PostgreSQLContainer<?> db =
new PostgreSQLContainer<>("postgres:16-alpine");
// static = ONE container for the whole class (and JVM, with reuse)
// Flyway runs the REAL migrations against the REAL engine ✅
@Autowired OrderRepository repo;
@Test void upsertUsesPostgresOnConflict() { // would be untestable on H2
repo.upsert(order);
repo.upsert(order.withTotal(Money.of("20")));
assertThat(repo.findBySku(order.sku())).hasSize(1);
}
}
// Local dev bonus: @TestConfiguration + @ServiceConnection containers
// power 'bootTestRun' — the app runs locally against containers too.- What the real engine catches that H2 can't: dialect-specific SQL (ON CONFLICT, window functions, JSONB), locking behavior, constraint error codes, collation/ordering, migration scripts themselves (your Flyway files finally get tested).
- Speed levers: singleton containers (start once per JVM —
static+ reuse),.withReuse(true)for local dev (daemon containers surviving runs), tmpfs for data dirs, Alpine images, and pulling images in a CI warm-up step. - Kafka/Redis/etc. too: the pattern generalizes;
@ServiceConnectioncovers many connection types; for HTTP third-parties use WireMock (their API isn't yours to containerize). - CI reality: needs Docker-capable runners (or Testcontainers Cloud); flakiness usually = port/resource contention — fix with parallelism limits, not retries.