Skip to content

PATTERN Cited by 1 source

Real Docker container over in-memory fake

Pattern

For integration tests that cross a system boundary (database, AWS service, HTTP peer, queue, cache), run the real server as a Docker container via systems/testcontainers rather than an in-memory emulator (H2, embedded-Kafka, Redis-java, a hand-rolled fake).

Trade: pay the container startup cost (seconds) to get real engine semantics. Amortise the cost with the singleton container pattern.

Why

Emulators approximate their reference engine's SQL / protocol / error-codes but diverge on:

  • Engine-specific features — Postgres jsonb operators, LATERAL joins, declarative partitioning, PL/pgSQL stored procedures, full-text search, logical replication. H2 either doesn't implement these or implements them slightly wrong. See concepts/h2-vs-real-database-testing.
  • Error shapes — SQLSTATE codes, exception classes, constraint violation messages. Tests that pattern-match on these silently drift.
  • Behaviour under load / concurrency — lock acquisition, MVCC snapshot timing, queue fairness. In-memory fakes are single-threaded shortcuts.
  • Wire protocol edge cases — for HTTP peers, a MockServer or WireMock container lets you simulate exactly the HTTP codes, delays, and connection-drops that your application has to handle.
  • AWS service semantics — S3 eventual consistency windows, DynamoDB provisioned-capacity throttles, Kinesis partition key behaviour. systems/localstack is imperfect but closer to AWS than any hand-rolled fake; and it runs offline.

Mechanism

From Zalando ZMS (sources/2021-02-24-zalando-integration-tests-with-testcontainers):

  1. Declare the container in test code: new PostgreSQLContainer("postgres:13.1") with credentials.
  2. Start it once per JVM (static { container.start(); } on a shared base class — see patterns/shared-static-container-across-tests).
  3. Wire dynamic ports / credentials into the application context via Spring's @DynamicPropertySource (5.2.5+).
  4. Tests run against the real Postgres. Isolation is maintained by unique IDs or per-test cleanup.
  5. Rely on JVM shutdown hooks + Testcontainers' Ryuk reaper for cleanup; no explicit .stop() needed.

Cost

  • Startup latency: ~4 s Postgres, ~0.4 s H2, up to ~20 s Localstack on Zalando's author machine. Singleton pattern makes this a per-JVM tax, not a per-test tax.
  • Docker daemon requirement — devs and CI workers need a Docker-compatible runtime (Docker Desktop, Colima, Podman, rootless Docker).
  • Memory / CPU — bigger CI instances, especially for Localstack or multi-container suites.

When not to use

  • Pure unit tests that never execute SQL / HTTP — use a real unit test with mocks instead.
  • Fast developer inner loops — the mvn test (Surefire) pass should stay under seconds. Put Testcontainers-backed tests behind patterns/failsafe-integration-test-separation so they only run on CI and explicit mvn verify -P with-integration-tests.
  • Test counts so high that even singleton-shared Docker is the bottleneck — at that point the pyramid shape (concepts/test-pyramid) is wrong and too many ITs exist relative to units.

What it still doesn't cover

  • Real-API drift. A MockServer/WireMock container tests that your code works against that mock, not against the real external peer, which may have changed. Pair with concepts/contract-testing.
  • Geographic / network-latency effects. Local container latency is ~100 μs; real AWS is ~10–100 ms.
  • Full-stack interactions. System / E2E tests against the deployed stack remain necessary but rare.

Seen in

Last updated · 476 distilled / 1,218 read