Skip to content

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:

Performance comparison
| 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 test or gradle 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:

AbstractIntegrationTest.java
@SpringBootTest
public 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:

UserRepositoryTest.java
class UserRepositoryTest extends AbstractIntegrationTest {
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndFindUser() {
User user = new User("[email protected]");
userRepository.save(user);
assertThat(userRepository.findByEmail("[email protected]")).isPresent();
}
}

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.properties
testcontainers.reuse.enable=true

Or set the environment variable:

Terminal
export TESTCONTAINERS_REUSE_ENABLE=true

Implementation

Create a test configuration with withReuse(true):

ReusableContainerConfig.java
@TestConfiguration
public class ReusableContainerConfig {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>("postgres:16-alpine")
.withReuse(true);
}
}

Or use the JDBC URL approach:

MyIntegrationTest.java
@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:

ContainerConfiguration.java
@TestConfiguration
public 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:

BaseIntegrationTest.java
@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:

UserServiceTest.java
class UserServiceTest extends BaseIntegrationTest {
@Autowired
private UserService userService;
@Test
void testUserService() {
// Uses cached Spring context
}
}
OrderServiceTest.java
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:

Bad example - breaks context caching
@SpringBootTest
class TestA {
@MockBean UserService userService; // Creates Context A
}
@SpringBootTest
class 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 comparison
| 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:

  1. For CI: Use the singleton pattern. It’s reliable and predictable.
  2. For local development: Combine withReuse(true) with the singleton pattern.
  3. For Spring Boot 3.1+: Use @TestConfiguration with @ServiceConnection for the cleanest code.

Combining Patterns for Maximum Performance

You can combine the singleton and withReuse patterns:

OptimizedContainerConfig.java
@TestConfiguration
public 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:

Problem: tests interfere with each other
class TestA extends BaseTest {
@Test
void test() {
repository.save(new User("[email protected]"));
// 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:

  1. Use @Transactional on test methods. Spring rolls back after each test.
  2. Clean the database in @BeforeEach.
  3. 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.properties
testcontainers.reuse.enable=true

Without 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:

Wrong approach
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:

Correct approach
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:

Benchmark results
| 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 final container with static initializer
  • Use @DynamicPropertySource for Spring properties
  • All test classes extend the base class
  • Don’t use @Container annotation on static field

withReuse Pattern

  • Add testcontainers.reuse.enable=true to ~/.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 @TestConfiguration with @ServiceConnection
  • Import same config in all tests
  • Avoid @MockBean differences 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