Interview Prep Zoneby CuriouserLabs

Question 3 of 5 · staff level

How do you use Testcontainers well? H2-vs-real-database, @ServiceConnection, and CI performance.

🎤 Say this first

Testcontainers runs the real dependency (Postgres, Kafka, Redis) in Docker per suite — killing the H2 lie: H2 accepts different SQL, misses vendor functions/locking/JSON types, so green-on-H2 proves little for a Postgres app. Boot 3.1+ @ServiceConnection wires container → datasource automatically (no URL plumbing). Performance discipline: reuse containers across tests (static/singleton pattern), one container set per suite not per class, parallel-safe schemas, and CI with layer-cached images — a well-tuned integration suite runs in tens of seconds.

The full picture

The modern setup
@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; @ServiceConnection covers 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.

🔄 Likely follow-up questions

  • How do you share one container across many test classes safely?
  • What's your strategy for test data — per-test truncation vs transactions vs unique keys?
  • How does @ServiceConnection actually replace @DynamicPropertySource?