Skip to content

PATTERN Cited by 1 source

Shared static container across tests

Pattern

Hold a Testcontainers container in a public static field on an abstract base test class, start it once in a static initialiser, and have every concrete integration test extend the base. All ITs share the same container instance across the JVM process.

Implements the singleton container pattern for JVM integration test suites.

Shape

@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();          // once per JVM
    }

    @DynamicPropertySource
    static void postgresqlProperties(DynamicPropertyRegistry r) {
        r.add("db_url", postgreSQL::getJdbcUrl);
        r.add("db_username", postgreSQL::getUsername);
        r.add("db_password", postgreSQL::getPassword);
    }
}

class AccountRepositoryIntegrationTest extends AbstractIntegrationTest {
    @Autowired AccountRepository dao;

    @Test
    void shouldCreateAccount() { /* uses shared DB */ }
}

Source: sources/2021-02-24-zalando-integration-tests-with-testcontainers

Mechanism

  • static field is class-loader-scoped: evaluated once per JVM, shared by every subclass.
  • static { container.start(); } initialiser runs on first class load, before any @BeforeAll fires.
  • @DynamicPropertySource (Spring 5.2.5+) wires dynamic JDBC URL / random-port values into the Spring test context after the container has started. Method references on the container (postgreSQL::getJdbcUrl) defer evaluation until Spring asks.
  • No .stop(). Testcontainers registers a JVM shutdown hook; the Ryuk reaper (itself a Docker container) cleans up any orphaned images if the JVM dies before shutdown hooks run.

Contrast with @Testcontainers / @Container

The JUnit Jupiter Testcontainers module provides annotation-based lifecycle:

@Testcontainers
public class ApplicationIntegrationTest {
    @Container
    public static PostgreSQLContainer postgreSQL = new PostgreSQLContainer(...);
}

This is per-class, not per-JVM. With N IT classes you pay N container startups. The Testcontainers docs explicitly note: "containers cannot be reused between test classes" and "this extension has only been tested with sequential test execution". The shared-static-base-class pattern avoids both limitations at the cost of inheritance coupling.

Tradeoffs

  • Shared DB state across tests. See FIRST. Two strategies to keep isolation:
    • Unique IDs per test (UUID.randomUUID(), account-<ts>), no cleanup. Aggregate queries see other tests' rows.
    • @AfterEach cleanup. More developer discipline needed.
  • Inheritance is sticky. Every IT must extend AbstractIntegrationTest. Teams that can't use inheritance (shared composition across different test superclasses) need an alternative — often a JUnit 5 extension or a @Singleton Spring bean.
  • Slow first test. The first test to trigger base-class loading pays the ~4 s Postgres startup (20 s Localstack); subsequent tests are fast.
  • One container per container type. To run DB + Localstack + MockServer, declare three static fields on the base. All start sequentially in the static initialiser (or in parallel if you declare them .withStartupTimeout(...) and use Startables.deepStart(...)).
  • No parallel-test safety inside the JVM. Two tests that write to the same Postgres row race unless isolation rules are enforced.

Alternatives

  • @Container per class — isolation by construction, pay startup per class.
  • Testcontainers Desktop / .withReuse(true) — container persists across JVM runs. Adds new failure modes (stale state between unrelated runs) but cuts first-test latency near zero. Not discussed in Zalando's 2021 post.
  • Dynamic @DynamicPropertySource from bean factory — for teams avoiding inheritance; works with JUnit 5 extension model.

Seen in

Last updated · 476 distilled / 1,218 read