ML
Testing

Testcontainers: Real Dependencies, Reproducible Tests

Spinning up real Postgres + Kafka + Redis for every test sounds expensive. It isn't, if you do reuse and singletons right.

April 18, 20268 min readTestingTooling

Testcontainers is the library that took "I'll spin up a real database for this test" from aspirational to fast enough that it's the default. It runs Docker containers from your test code, waits for them to be healthy, hands you the connection details, and tears them down at the end.

1. The naive use (and why it's slow)

@BeforeEach
void setup() {
  postgres = new PostgreSQLContainer<>("postgres:16");
  postgres.start();
  // ... migrate, seed
}

Container per test. Each one takes 2–5 seconds to start. A suite of 500 tests = 30 minutes of container startup. This is where "testcontainers is slow" rumours come from.

2. The right shape: one container per suite

// JUnit 5: singleton container for the whole class hierarchy
static final PostgreSQLContainer<?> POSTGRES =
  new PostgreSQLContainer<>("postgres:16")
    .withReuse(true);

static { POSTGRES.start(); }

The container starts once, all tests share it. Per-test, you isolate via transactions:

@BeforeEach
void beginTransaction() { jdbc.execute("BEGIN"); }
@AfterEach
void rollback() { jdbc.execute("ROLLBACK"); }

Every test sees a clean DB state, but the container itself only starts once. 500 tests now take about as long as 500 transactions — under a minute.

3. Reuse across runs with TC_REUSE

Set testcontainers.reuse.enable=true in ~/.testcontainers.properties and tag the container .withReuse(true). The container survives between test runs on the same machine. Now incremental local development is almost instant — your Postgres is just sitting there.

CI usually keeps the default (no reuse) so each build starts clean.

4. Beyond Postgres: composing real systems

The pattern composes. A common shape for a backend integration suite:

Network net = Network.newNetwork();

PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:16")
  .withNetwork(net).withNetworkAliases("db");

KafkaContainer kafka = new KafkaContainer(
  DockerImageName.parse("confluentinc/cp-kafka:7.6.0"))
  .withNetwork(net);

GenericContainer redis = new GenericContainer("redis:7-alpine")
  .withExposedPorts(6379).withNetwork(net);

postgres.start(); kafka.start(); redis.start();

The whole stack starts in parallel. Your application connects to postgres.getJdbcUrl(), kafka.getBootstrapServers(), redis.getMappedPort(6379). Tear-down is automatic at JVM exit.

5. The traps

5.1 Schema drift between test and prod

Run your real migrations against the container, every test run. If the container is reused, migrate idempotently. The fastest way to ship a broken migration is to seed test DBs from a SQL dump and skip the migration in tests.

5.2 Long pull times in CI

The first run on a new CI image downloads images. Pre-warm them in the CI base image. A 200 MB Postgres pull turning into 2 GB of layers because someone FROM'd a heavy base is real, and visible in the first build of the morning.

5.3 Mismatched versions

Pin the image version. postgres:latest in tests, postgres:16 in prod = the day Postgres 17 ships, your tests break "spontaneously."

5.4 Port collisions

Testcontainers maps ports randomly by default. Use getMappedPort(...) rather than hard-coding 5432 in connection strings.

6. When testcontainers is wrong

  • Pure logic units — no DB needed, no container needed. Run them in ms.
  • Cloud-only services (DynamoDB, BigQuery) — use a local emulator if one exists (LocalStack covers many AWS services); otherwise fall back to a thin abstraction + recorded fixtures.
  • Anything that cannot run in Docker at all (Apple Silicon + some older images can be painful).

The win

When integration tests against real Postgres run in 50ms per test, the entire "we use mocks because real DBs are slow" argument collapses. The team writes more honest tests, catches more real bugs, and ships faster. Testcontainers is one of the few infrastructure-of-testing investments that pays back inside a month.

SharePostLinkedIn

Reader Discussion

2 replies// weighed in

TopNewestAuthor
Add to the thread
Disagree, agree harder, or share your own experience…
Email instead →markdown okbe kind
  1. Léa Dubois· SREAsks

    any chance you'd publish these as a PDF collection? would love to print and read offline on flights. screen-fatigue is real.

    Apr 24, 2026·6 days later
  2. Ahmed Rahman· Full StackKind words

    concise + opinionated = my favourite kind of engineering post. so many blogs hedge every claim into mush. give me the spicy take with the receipts. more please.

    Apr 19, 2026·1 day later

Worked on something similar? Email ducminhldm@gmail.com — I read every one. The good ones become future posts.

Comments seeded · live discussion via email