With one Testcontainers Postgres container shared by a whole suite, how do you stop tests leaking state?
answer
- Sharing moves a cost, not removes it
- Whose job is a clean database now?
- Reset, partition, or roll back
- Clean before the test, not after
- Passes alone, fails in the suite
basics
~10 sSharing a container makes cleanup the test's own job: truncate or recreate the data each test touches, or give each test its own schema or uniquely named resources. Never depend on execution order.
solid answer
~60 sOnce a container is shared, isolation stops being free and becomes a contract every test has to honour. Three approaches, cheapest first. **Reset the data**: truncate the tables a test touches in a `@BeforeEach`, which is fast and blunt. **Partition the namespace**: give each test class its own schema or database, or generate unique keys, topic names and identifiers per test, so tests simply cannot see each other's rows. **Roll back**: wrap the test in a transaction that never commits — fast and clean, but only valid when the code under test does not manage its own transactions or spawn threads. Do the cleanup at the **start** of a test rather than only at the end, so a run that crashes halfway does not poison the next one. Migrations run once, when the container starts, not per test. The failure mode to name explicitly is the test that passes alone and fails in a full run, or only in a different order — that is always shared state, not flakiness.
code
kotlin · 8 lines@BeforeEach
fun resetData() {
postgres.createConnection("").use { conn ->
conn.createStatement().use { st ->
st.execute("TRUNCATE app_user, orders RESTART IDENTITY CASCADE")
}
}
}go deeper
Recall that a shared container means a shared database, so tests must clean or namespace the data they use instead of assuming it is empty.
Explain the three strategies — truncate, per-test schema or unique names, and transactional rollback — and where each one stops working.
Diagnose an order-dependent failure as a state leak rather than flakiness, and choose a cleanup contract that survives crashed tests and container reuse.
Own isolation as a suite-wide standard: one documented approach, enforced in shared test infrastructure, so hundreds of tests do not each invent their own.
## Sharing converts isolation into a contract A fresh container per test gives perfect isolation at an unaffordable price. Every sharing strategy — one container per class, one per JVM, or one reused across runs — buys speed by making the data outlive the test. From that moment on, isolation is something your tests must produce, not something the container gives them. ## Strategy 1: reset the data Before each test, truncate the tables involved. It is simple, it is explicit, and for a handful of tables it costs milliseconds. Two details matter: truncate everything the test *reads*, not just what it writes, or a leftover row will satisfy a query it should not; and restart identity sequences if your assertions depend on generated ids. The weakness is maintenance — a new table added by a migration is silently absent from the truncate list, which is exactly the sort of bug that appears months later as a mysterious ordering dependency. Deriving the table list from the catalogue rather than hardcoding it avoids that. ## Strategy 2: partition the namespace Instead of cleaning shared data, avoid sharing it. Give each test class its own schema (or database) inside the same container and point its connections at it; the container is still shared, so you keep the speed, but the rows cannot collide. The same idea works outside relational stores: generate unique topic names, key prefixes or bucket names per test. This scales better than truncation in large suites and is the only approach that survives concurrent execution comfortably, at the cost of some setup machinery and, for per-class schemas, running migrations more than once. ## Strategy 3: roll back Run the test inside a transaction and roll it back at the end. Nothing is ever committed, so nothing leaks and cleanup is free. The constraints are real though: it only works if the code under test participates in your transaction rather than opening its own, it hides commit-time behaviour such as deferred constraints and triggers, and it breaks the moment the code under test uses another thread or connection. ## Clean at the start, not only at the end Teardown-only cleanup is one crashed test away from useless: the run that failed to clean up is not the run that suffers. Cleaning before each test makes every test's precondition explicit and independent of what happened before it. This becomes mandatory once container reuse is in play, because the state you inherit may be from yesterday's run. ## Where one-time setup goes Schema migrations and reference data belong to container startup, executed once. Running them per test is slow and, with any concurrency, racy. The dividing line is useful: structure is created once per container; data is managed per test. ## The diagnostic When a suite passes class-by-class but fails as a whole, or fails only when the order changes, do not reach for retries. Shuffle the execution order deliberately and confirm the failure moves — that identifies a state leak rather than a timing problem, and the fix is a cleanup contract, not a longer timeout. Retries applied here hide the bug and eventually hide a real one. ## Interview framing Strong answers pick a strategy, justify it against the suite's shape, and name what it cannot do. Weak answers say "we clean up in @AfterEach" without noticing that a crashed test never runs its teardown.
- A suite passes class by class but fails when run as a whole. How do you confirm it is shared state?Run the classes in a deliberately different order and see whether the failure moves with the ordering — a state leak follows the order, a genuine timing problem does not. Then narrow to the pair of tests involved and inspect what the first leaves behind. Resist adding retries: they convert a reproducible ordering bug into an intermittent one.
- When is transactional rollback the wrong isolation strategy?When the code under test manages its own transactions, commits explicitly, or does work on another thread or connection — the rollback then covers nothing, or the code deadlocks against your open transaction. It also hides commit-time behaviour such as deferred constraints and after-commit hooks, so anything you want to assert about committed state needs real cleanup instead.
- Should schema migrations run per test or per container?Per container, once, at startup. They define structure, which is shared and stable; tests manage data, which is not. Re-running migrations per test is slow, and with concurrent tests it races. If a test genuinely needs a different schema shape, give it its own schema rather than re-migrating the shared one.
saying these in an interview costs you the question
- Cleans up only in teardown, so crashed tests poison the next
- Relies on test execution order staying the same
- Adds retries to hide an ordering-dependent failure
- Re-runs migrations before every test method
- Truncates only the tables the test writes to