CONCEPT Cited by 1 source
Singleton container pattern¶
Definition¶
A singleton container is a Docker container declared once
per JVM test process, started in a static initialiser, held in
a static field, and shared across every test that needs it.
Contrast with the default Testcontainers lifecycle (one
container per test class, or even per test method via
@Container), which pays Docker startup cost repeatedly.
Named and documented in the Testcontainers manual-lifecycle docs.
Shape (Zalando ZMS, verbatim)¶
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public abstract class AbstractIntegrationTest {
public static PostgreSQLContainer postgreSQL =
new PostgreSQLContainer("postgres:13.1")
.withUsername("testUsername")
.withPassword("testPassword")
.withDatabaseName("testDatabase");
static {
postgreSQL.start();
}
@DynamicPropertySource
static void postgresqlProperties(DynamicPropertyRegistry r) {
r.add("db_url", postgreSQL::getJdbcUrl);
r.add("db_username", postgreSQL::getUsername);
r.add("db_password", postgreSQL::getPassword);
}
}
Every concrete IT extends AbstractIntegrationTest and inherits
the already-started container. The container is never
explicitly stopped — the Testcontainers library registers a
JVM shutdown hook, and the
Ryuk companion container
reaps orphans if the JVM dies before the hook fires.
Why static + static-initialiser instead of @BeforeAll¶
@BeforeAll is per-class: JUnit runs it once per test class,
so with N IT classes you still pay N container startups. A
static field on a shared base class is class-loader-scoped,
evaluated once per JVM. Exactly one container startup, no
matter how many IT subclasses.
Why @DynamicPropertySource (Spring ≥ 5.2.5)¶
Testcontainers gives the container a dynamic JDBC URL
(random host port, URL-encoded password) at start time. Static
application-test.properties can't reference those values —
they don't exist until .start() runs.
@DynamicPropertySource registers suppliers (via method
reference: postgreSQL::getJdbcUrl) that Spring resolves at
context-creation time, after the container has booted. More
compact than the older ApplicationContextInitializer
approach. (Source:
sources/2021-02-24-zalando-integration-tests-with-testcontainers)
Tradeoffs¶
- Shared state across tests. The defining trade: ITs now
share a live DB / queue / cache. The two strategies for
keeping tests FIRST on a
shared container:
- Unique IDs per test (no cleanup needed, but
COUNT(*)sees other tests). - Per-test cleanup (
@AfterEachdeletes rows, drops topics). More developer effort, order-sensitive if forgotten.
- Unique IDs per test (no cleanup needed, but
- First-test penalty. The first test to trigger class loading pays the full startup cost (~4 s Postgres); subsequent tests are fast.
- Parallelism hazard. If tests are run concurrently within
the JVM and both mutate the same container, races appear.
Zalando quotes the Testcontainers caveat: the
@Testcontainersannotation "has only been tested with sequential test execution". - No hot reload. If a test mutates the DB schema and leaves it mutated, subsequent tests see the mutation. Per-test rollback-in-transaction is a common Spring-specific mitigation.
Alternatives¶
- Per-class
@Testcontainers+@Containerstatic field — one container per test class, shared across methods, not shared across classes. Documented explicitly in the JUnit Jupiter Testcontainers integration as an intentional limitation. - Per-method container — maximum isolation, maximum startup cost. Usually unjustifiable except for tests that fundamentally require fresh state.
- Reuse across JVMs via
.withReuse(true)— keeps the container running between test process runs (Testcontainers feature added post-2020, not covered in the Zalando post).
Seen in¶
- sources/2021-02-24-zalando-integration-tests-with-testcontainers —
Zalando ZMS's
AbstractIntegrationTestbase-class idiom, sharing aPostgreSQLContaineracross every concrete IT class.
Related¶
- systems/testcontainers — the library.
- patterns/shared-static-container-across-tests — the pattern this concept realises.
- concepts/first-test-principles — the discipline that shared state threatens.
- concepts/h2-vs-real-database-testing — the sibling decision about engine fidelity.