How to Reuse Testcontainers Across Spring Boot Test Classes
Your integration tests take 2 minutes to run. The actual test logic? 10 seconds. The rest is waiting for Docker containers to start.
I faced this exact problem. Every test class started a fresh PostgreSQL container. Each startup took 30-60 seconds. With 10 test classes, I spent 5-10 minutes just watching containers start and stop.
In this post, I’ll show you three patterns to reuse Testcontainers across Spring Boot test classes. You’ll learn when to use each approach and how to avoid common pitfalls.
Why Container Reuse Matters
Here’s the performance impact I measured:
| Scenario | Container Starts | Total Overhead ||---------------------|------------------|-----------------|| Default (per class) | 10 classes = 10 | ~5-10 minutes || Singleton pattern | 1 start | ~30-60 seconds || withReuse | 1 per session | ~30s first only |The difference is massive. When you reuse containers, you:
- Cut Docker daemon load significantly
- Reduce memory usage during test runs
- Get faster feedback in development
The key insight is this: start the container once and share it between test contexts.
Solution 1: Singleton Container Pattern
This is the simplest approach. Use it when:
- You run tests in a single JVM process (one
mvn testorgradle test) - Multiple test classes need the same container
- You want a setup without external configuration
How It Works
Create a base test class with a static container:
@SpringBootTestpublic abstract class AbstractIntegrationTest {
private static final PostgreSQLContainer<?> POSTGRES;
static { POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); POSTGRES.start(); }
@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); }}Now your test classes extend this base:
class UserRepositoryTest extends AbstractIntegrationTest {
@Autowired private UserRepository userRepository;
@Test void shouldSaveAndFindUser() { userRepository.save(user);
}}Why This Works
The static initializer block runs once when the class loads. All test classes that extend AbstractIntegrationTest share the same container instance. The Ryuk container (Testcontainers’ cleanup mechanism) stops everything at the end of the test suite.
Important: Don’t use @Container annotation on static fields in your base class. That creates a per-class lifecycle. Use manual start() in a static block instead.
Solution 2: withReuse(true) Pattern
This pattern keeps containers running across test sessions. It’s perfect for local development when you run tests frequently.
Enable Reuse Globally
First, create or edit this file in your home directory:
testcontainers.reuse.enable=trueOr set the environment variable:
export TESTCONTAINERS_REUSE_ENABLE=trueImplementation
Create a test configuration with withReuse(true):
@TestConfigurationpublic class ReusableContainerConfig {
@Bean @ServiceConnection PostgreSQLContainer<?> postgresContainer() { return new PostgreSQLContainer<>("postgres:16-alpine") .withReuse(true); }}Or use the JDBC URL approach:
@SpringBootTest@TestPropertySource(properties = { "spring.datasource.url=jdbc:tc:postgresql:16:///testdb?TC_REUSABLE=true"})class MyIntegrationTest { // Your test logic}Key Points
The container persists until you manually stop it or Docker restarts. Testcontainers matches containers by configuration hash. Same config equals same container.
Warning: Don’t call stop() or use try-with-resources on reusable containers. That defeats the purpose.
CI Consideration: This pattern works best locally. In CI environments with fresh runners, use the singleton pattern instead.
Solution 3: Spring Boot Context Caching
If you use Spring Boot 3.1+, you can leverage @ServiceConnection with context caching.
How Context Caching Works
Spring’s test framework caches application contexts between tests. When your tests share the same configuration, Spring loads the context only once.
Create a shared configuration:
@TestConfigurationpublic class ContainerConfiguration {
@Bean @ServiceConnection PostgreSQLContainer<?> postgres() { return new PostgreSQLContainer<>("postgres:16-alpine"); }
@Bean @ServiceConnection GenericContainer<?> redis() { return new GenericContainer<>("redis:7-alpine") .withExposedPorts(6379); }}Create a base test class:
@SpringBootTest@Import(ContainerConfiguration.class)public abstract class BaseIntegrationTest { // Common test setup if needed}Now all tests that extend BaseIntegrationTest share the same Spring context and container:
class UserServiceTest extends BaseIntegrationTest {
@Autowired private UserService userService;
@Test void testUserService() { // Uses cached Spring context }}class OrderServiceTest extends BaseIntegrationTest {
@Autowired private OrderService orderService;
@Test void testOrderService() { // Reuses same Spring context, same container }}Pitfall: Breaking Context Cache
Different configurations create different contexts. This breaks caching:
@SpringBootTestclass TestA { @MockBean UserService userService; // Creates Context A}
@SpringBootTestclass TestB { @MockBean OrderService orderService; // Creates Context B (different!)}Each different @MockBean or @TestConfiguration creates a new context. Use @DirtiesContext sparingly. It invalidates the cache entirely.
Which Pattern Should You Choose?
| Pattern | Scope | Container Lifetime | Best For ||------------------------|----------------|-----------------------|-----------------------------|| Singleton (static) | Single run | JVM process | CI and local, simple setup || withReuse(true) | Cross sessions | Until stopped | Local dev, frequent reruns || @TestConfiguration | Single run | Spring context life | Spring Boot 3.1+ projects |My recommendation:
- For CI: Use the singleton pattern. It’s reliable and predictable.
- For local development: Combine
withReuse(true)with the singleton pattern. - For Spring Boot 3.1+: Use
@TestConfigurationwith@ServiceConnectionfor the cleanest code.
Combining Patterns for Maximum Performance
You can combine the singleton and withReuse patterns:
@TestConfigurationpublic class OptimizedContainerConfig {
private static final PostgreSQLContainer<?> SHARED_POSTGRES;
static { SHARED_POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine") .withReuse(true); SHARED_POSTGRES.start(); }
@Bean @ServiceConnection static PostgreSQLContainer<?> postgresContainer() { return SHARED_POSTGRES; }}This gives you:
- One container per JVM (singleton)
- Container persists across test runs (
withReuse) - Automatic Spring Boot integration (
@ServiceConnection) - Fast Spring context reuse (context caching)
Common Pitfalls and How to Avoid Them
Pitfall 1: Database State Pollution
When you reuse containers, data persists between tests. This can cause test interference:
class TestA extends BaseTest { @Test void test() { // User stays in the shared container! }}
class TestB extends BaseTest { @Test void test() { // Might see User from TestA assertThat(repository.count()).isEqualTo(0); // FAILS! }}Solutions:
- Use
@Transactionalon test methods. Spring rolls back after each test. - Clean the database in
@BeforeEach. - Use different schemas per test class.
Pitfall 2: Forgetting to Enable Reuse
You added withReuse(true) but containers still stop? You probably forgot the global setting.
testcontainers.reuse.enable=trueWithout this, withReuse(true) has no effect.
Pitfall 3: Using @Container in Base Class
Using @Container on a static field in a base class seems logical. But it doesn’t work as expected:
public abstract class BaseTest { @Container // This doesn't share across classes! static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");}Each test class that extends this gets its own container. Use a static initializer block instead:
public abstract class BaseTest { private static final PostgreSQLContainer<?> POSTGRES;
static { POSTGRES = new PostgreSQLContainer<>("postgres:16"); POSTGRES.start(); }}Performance Results
I tested these patterns with 10 test classes and a PostgreSQL container:
| Approach | First Run | Subsequent Runs | Total (10 classes) ||-------------------|------------------|-----------------|--------------------|| Default (per cls) | 60s overhead x10 | Same | ~10 minutes || Singleton | 60s overhead x1 | 60s x1 | ~1 minute || withReuse (local) | 60s first run | 0s after | ~1 min then instant|| Context caching | 60s + Spring init| Same context | ~1-2 minutes |The singleton pattern gave me a 10x improvement. With withReuse, my second test run was almost instant.
Quick Implementation Checklist
Singleton Pattern
- Create abstract base test class
- Define
static finalcontainer with static initializer - Use
@DynamicPropertySourcefor Spring properties - All test classes extend the base class
- Don’t use
@Containerannotation on static field
withReuse Pattern
- Add
testcontainers.reuse.enable=trueto~/.testcontainers.properties - Call
.withReuse(true)on your container - Start container manually or via Spring Boot
- Don’t call
stop()or use try-with-resources - Container config must match exactly for reuse
Context Caching Pattern
- Use
@TestConfigurationwith@ServiceConnection - Import same config in all tests
- Avoid
@MockBeandifferences between tests - Avoid
@DirtiesContext - Use Spring Boot 3.1+ for best experience
Summary
Reusing Testcontainers dramatically speeds up your test suite. The singleton pattern is the simplest and most reliable approach. For local development, add withReuse(true) to keep containers running between sessions.
Start by measuring your current test suite time. Implement the singleton pattern first. Then add optimizations based on your needs.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments