Skip to content

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 (@AfterEach deletes rows, drops topics). More developer effort, order-sensitive if forgotten.
  • 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 @Testcontainers annotation "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 + @Container static 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

Last updated · 476 distilled / 1,218 read