Appearance
Fresh
Singleton containers
Back
Testcontainers container lifecycle management using JUnit 5
Learn different approaches to manage container lifecycle with Testcontainers using JUnit 5 lifecycle callbacks, extension annotations, and the singleton containers pattern.
Java Testing with Docker
20 minutes
Singleton containers pattern
Copy as Markdown
Open MarkdownAsk Docs AIClaudeOpen in Claude
Table of contents
As the number of test classes grows, starting containers for each class adds up. The singleton containers pattern starts all required containers once in a common base class and reuses them across all integration tests.
Define the base class
Create an abstract base class that starts the containers in a static initializer:
java
package com.testcontainers.demo;
import org.testcontainers.postgresql.PostgreSQLContainer;
import org.testcontainers.kafka.ConfluentKafkaContainer;
public abstract class AbstractIntegrationTest {
static PostgreSQLContainer postgres = new PostgreSQLContainer(
"postgres:16-alpine");
static ConfluentKafkaContainer kafka = new ConfluentKafkaContainer(
"confluentinc/cp-kafka:7.8.0");
static {
postgres.start();
kafka.start();
}
}The containers start once when the class loads and Testcontainers uses the Ryuk container to remove them after the JVM exits.
Tip
Instead of starting containers sequentially, start them in parallel using
Startables.deepStart(postgres, kafka).join();
Extend the base class
Each test class inherits from the base class and reuses the same containers:
java
class ProductControllerTest extends AbstractIntegrationTest {
ProductRepository productRepository;
@BeforeEach
void setUp() {
productRepository = new ProductRepository(...);
productRepository.deleteAll();
}
@Test
void shouldGetAllProducts() {
// test logic using the shared postgres container
}
}Avoid a common misconfiguration
A common mistake is combining singleton containers with the @Testcontainers and @Container annotations:
java
// DON'T DO THIS — containers will stop after each test class
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public abstract class AbstractIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
DockerImageName.parse("postgres:16-alpine"));
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}The @Testcontainers extension stops containers at the end of each testclass. Subsequent test classes reuse the cached Spring context, but the containers are already stopped — causing connection failures.
Instead, use a static initializer or @BeforeAll to start the containers, without the @Testcontainers and @Container annotations.
Summary
- Use JUnit 5 lifecycle callbacks (
@BeforeAll/@AfterAll) for explicit control over container startup and shutdown. - Use extension annotations (
@Testcontainers/@Container) for less boilerplate in single test classes. - Use the singleton containers pattern (static initializer in a base class) to share containers across multiple test classes.
- Don't mix singleton containers with
@Testcontainers/@Containerannotations.