Share feedback
Answers are generated based on the documentation.

Singleton containers pattern

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:

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:

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:

// 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 test class. 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/@Container annotations.

Further reading