Container Providers Example
This example demonstrates the use of @ContainerProvider and @ContainerConfig annotations to share containers across multiple test classes.
Problem
When you have multiple integration test classes that need the same container (e.g., a database), starting a new container for each test class is slow and wasteful. The traditional singleton pattern requires boilerplate code with static initializers.
Solution
Container providers allow you to:
1. Define containers once using @ContainerProvider
2. Reference them by name using @ContainerConfig
3. Share containers across test classes automatically
4. Control lifecycle with scopes (CLASS or GLOBAL)
Example Structure
src/test/java/
├── BaseIntegrationTest.java # Base class with shared providers
├── UserServiceIntegrationTest.java # Test class using shared database
├── OrderServiceIntegrationTest.java # Another test class using same database
└── PaymentServiceIntegrationTest.java # Yet another test using same database
Key Benefits
Before (Manual Singleton Pattern)
abstract class BaseIntegrationTest {
static final PostgreSQLContainer<?> POSTGRES;
static {
POSTGRES = new PostgreSQLContainer<>("postgres:14");
POSTGRES.start();
}
}
class UserServiceTest extends BaseIntegrationTest {
@Test
void test() {
String jdbcUrl = POSTGRES.getJdbcUrl();
// ...
}
}
Issues: - Boilerplate static initializer code - Manual lifecycle management - No type-safe parameter injection - Hard to control when containers start/stop
After (Container Providers)
abstract class BaseIntegrationTest {
@ContainerProvider(name = "database", scope = Scope.GLOBAL)
public PostgreSQLContainer<?> createDatabase() {
return new PostgreSQLContainer<>("postgres:14");
}
}
@Testcontainers
class UserServiceTest extends BaseIntegrationTest {
@Test
@ContainerConfig(name = "database", injectAsParameter = true)
void test(PostgreSQLContainer<?> db) {
String jdbcUrl = db.getJdbcUrl();
// ...
}
}
Benefits: - ✅ No boilerplate - ✅ Automatic lifecycle management - ✅ Type-safe parameter injection - ✅ Declarative configuration - ✅ Flexible scoping
Running the Example
# Run all tests
./gradlew :junit-jupiter:test
# Run specific test
./gradlew :junit-jupiter:test --tests ContainerProviderBasicTests
Performance Comparison
Without Container Providers
- UserServiceTest: Start DB (5s) + Run tests (2s) = 7s
- OrderServiceTest: Start DB (5s) + Run tests (2s) = 7s
- PaymentServiceTest: Start DB (5s) + Run tests (2s) = 7s
- Total: 21 seconds
With Container Providers (GLOBAL scope)
- Start DB once (5s)
- UserServiceTest: Run tests (2s)
- OrderServiceTest: Run tests (2s)
- PaymentServiceTest: Run tests (2s)
- Total: 11 seconds (48% faster!)
Advanced Usage
Multiple Containers
@ContainerProvider(name = "postgres", scope = Scope.GLOBAL)
public PostgreSQLContainer<?> createPostgres() {
return new PostgreSQLContainer<>("postgres:14");
}
@ContainerProvider(name = "redis", scope = Scope.GLOBAL)
public GenericContainer<?> createRedis() {
return new GenericContainer<>("redis:6.2");
}
@Test
@ContainerConfig(name = "postgres", injectAsParameter = true)
void testDatabase(PostgreSQLContainer<?> db) { }
@Test
@ContainerConfig(name = "redis", injectAsParameter = true)
void testCache(GenericContainer<?> cache) { }
Test Isolation
@Test
@ContainerConfig(name = "database", needNewInstance = true)
void testWithFreshDatabase(PostgreSQLContainer<?> db) {
// Gets a brand new database instance
// Useful for tests that modify schema or data
}
Mixing with Traditional @Container
@Testcontainers
class MixedTest {
@Container
static final GenericContainer<?> TRADITIONAL =
new GenericContainer<>("httpd:2.4");
@ContainerProvider(name = "modern", scope = Scope.CLASS)
public GenericContainer<?> createModern() {
return new GenericContainer<>("redis:6.2");
}
// Both approaches work together!
}
Best Practices
- Use GLOBAL scope for expensive containers (databases, message queues)
- Use CLASS scope for lightweight containers that need isolation
- Use needNewInstance=true for tests that modify container state
- Define providers in base classes for cross-class sharing
- Use parameter injection for type-safe container access
Troubleshooting
Provider not found
ExtensionConfigurationException: No container provider found with name 'myContainer'
@ContainerProvider(name = "myContainer")
Duplicate provider names
ExtensionConfigurationException: Duplicate container provider name 'database'
Container returns null
ExtensionConfigurationException: Container provider method returned null