The Testcontainers JUnit Jupiter extension provides automatic lifecycle management for Docker containers in JUnit 5 tests. It enables integration tests with throwaway Docker containers for databases, message brokers, browsers, or any Docker-based service, ensuring test isolation and reproducibility without manual container management.
Required Dependencies:
org.testcontainers:junit-jupiter (this package, version 1.21.4)org.testcontainers:testcontainers core package is required (provided transitively)org.junit.jupiter:junit-jupiter-api (JUnit 5 API)Core Capabilities:
@Testcontainers - Annotation to activate automatic container lifecycle management on test classes@Container - Annotation to mark container fields for automatic management@EnabledIfDockerAvailable - Conditional test execution based on Docker availabilityTestcontainersExtension - JUnit 5 extension implementation (automatically registered via @Testcontainers)TestLifecycleAware interface for container-level test callbacksKey Interfaces and Classes:
@Testcontainers - Class-level annotation: disabledWithoutDocker(), parallel()@Container - Field-level annotation (no parameters)@EnabledIfDockerAvailable - Class or method-level annotation (no parameters)TestcontainersExtension - Extension class implementing BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, ExecutionConditionorg.testcontainers.lifecycle.Startableorg.testcontainers.lifecycle.TestLifecycleAware for callbacksDefault Behaviors:
@Container fields: Shared across all test methods in class (started once before first test, stopped after last test)@Container fields: Restarted for each test method (started before each test, stopped after each test)@Testcontainers(disabledWithoutDocker = false): Tests fail with exception if Docker unavailable (default)@Testcontainers(parallel = false): Containers start sequentially (default)BeforeAllCallback/BeforeEachCallback chain, late in AfterAllCallback/AfterEachCallback chain@Testcontainers annotation is inherited by subclasses (@Inherited)disabledWithoutDocker = true or @EnabledIfDockerAvailable is usedwithDatabaseName(), withEnv()) must be set before extension starts container (in field initializer)Threading Model:
@BeforeAll phase)@BeforeEach phase)getJdbcUrl(), getMappedPort()) are thread-safe for read operations once container is startedparallel = true) starts multiple containers concurrently but does not affect test execution order@Execution(ExecutionMode.CONCURRENT)) is unsupportedLifecycle:
@Container fields: Started once before any test method in the class (via BeforeAllCallback), stopped once after all tests complete (via AfterAllCallback)@Container fields: Started before each test method (via BeforeEachCallback), stopped after each test completes (via AfterEachCallback)TestLifecycleAware receive beforeTest() callback before each test and afterTest() callback after each testwithDatabaseName(), withEnv()) must be set before the extension starts the container (typically in field initializer)ExtensionConfigurationException for invalid configurationsdisabledWithoutDocker = true, tests are disabled (not failed) if Docker is unavailable@EnabledIfDockerAvailable is used, tests are skipped if Docker is unavailable@Inherited)@Nested) can have their own instance containers; static containers from outer class are accessibleCommon Patterns:
@Container field for expensive containers shared across tests@Container field for complete test isolation@Testcontainers(disabledWithoutDocker = true) or @EnabledIfDockerAvailable to skip tests when Docker unavailable@Testcontainers(parallel = true) for multiple independent containers@Testcontainers on base class, containers inherited by subclasses@Nested classes, access outer static containersTestLifecycleAware for container-level test callbacksIntegration Points:
@Test, @ParameterizedTest, @RepeatedTest, @TestFactory, @TestTemplate@BeforeAll, @BeforeEach, @AfterEach, @AfterAll (containers available in all callbacks)@Nested) - static containers from outer class accessible, instance containers in nested class@Testcontainers annotation inherited, containers from base class availableBeforeAllCallback/BeforeEachCallback, late for AfterAllCallback/AfterEachCallbacktestcontainers.reuse.enable=true) - reused containers not stopped by extensionorg.testcontainers.lifecycle.Startable (all Testcontainers container classes)org.testcontainers.lifecycle.TestLifecycleAware for test-level callbacksCritical Edge Cases:
@Testcontainers annotation is inherited; containers in base class are available to subclasses@RepeatedTest): Instance containers are restarted for each repetition; static containers are shared@TestFactory): Containers follow same lifecycle rules as regular tests@TestFactory returning DynamicTest): Instance containers are restarted for each dynamic test; static containers are shared@TestTemplate): Containers follow same lifecycle rules as regular testsTestcontainersExtension executes before most other extensions; containers are available in @BeforeAll and @BeforeEach callbacks@Container with manually managed containers (singleton pattern, try-with-resources)ExtensionConfigurationException if container field is nullException Handling:
ExtensionConfigurationException: Thrown when container field configuration is invalid:
Startable interfaceContainerLaunchException: Thrown by containers themselves if startup fails (Docker issues, image pull failures, health check timeouts)IllegalStateException: Thrown by containers if methods are called in wrong order (e.g., getJdbcUrl() before start())disabledWithoutDocker = true or @EnabledIfDockerAvailable is usedDecision Guidance:
parallel = true when: Multiple independent containers, container startup time is bottleneck, containers have no dependenciesdisabledWithoutDocker = true when: Tests should be skipped (not failed) if Docker unavailable, want graceful degradation@EnabledIfDockerAvailable when: Need method-level control, want explicit Docker requirement, using without @TestcontainersTestLifecycleAware when: Need container-level test callbacks, want to track test execution, need custom logging/metricsMaven:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.21.4</version>
<scope>test</scope>
</dependency>Gradle:
testImplementation 'org.testcontainers:junit-jupiter:1.21.4'import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.EnabledIfDockerAvailable;import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.containers.PostgreSQLContainer;
import org.junit.jupiter.api.Test;
@Testcontainers
class DatabaseTest {
// Shared container - started once for all tests
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine");
// Restarted container - fresh instance for each test
@Container
private MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@Test
void testWithContainers() {
// Containers are automatically started and available
String postgresUrl = postgres.getJdbcUrl();
String mysqlUrl = mysql.getJdbcUrl();
// Run your test logic
}
}The JUnit Jupiter extension provides a declarative container management model:
@Testcontainers annotation automatically registers the TestcontainersExtension with JUnit 5@ContainerTestLifecycleAware receive test lifecycle notificationsBeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, and ExecutionCondition from JUnit 5Activate the Testcontainers extension on test classes to enable automatic container lifecycle management.
/**
* Activates automatic startup and stop of containers used in a test case.
* Finds all fields annotated with @Container and manages their lifecycle.
*
* Static fields are shared between test methods (started once, stopped after all tests).
* Instance fields are restarted for each test method (started before each, stopped after each).
*
* This annotation is inherited by subclasses, enabling container management in test hierarchies.
*
* The extension executes in the following order relative to other extensions:
* - BeforeAllCallback: Executes early, before most other extensions
* - AfterAllCallback: Executes late, after most other extensions
* - BeforeEachCallback: Executes early, before most other extensions
* - AfterEachCallback: Executes late, after most other extensions
* - ExecutionCondition: Executes to determine if tests should run
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(TestcontainersExtension.class)
@Inherited
public @interface Testcontainers {
/**
* Whether tests should be disabled (rather than failing) when Docker is not available.
*
* When true, tests are skipped (disabled) if Docker cannot be accessed.
* When false (default), tests fail with an exception if Docker is unavailable.
*
* This applies to all test methods in the annotated class.
* For method-level control, use @EnabledIfDockerAvailable instead.
*
* @return true to disable tests when Docker unavailable, false to fail tests (default: false)
*/
boolean disabledWithoutDocker() default false;
/**
* Whether containers should start in parallel.
* Improves startup time when multiple containers are used in a test class.
*
* When true, all containers in the class start concurrently.
* When false (default), containers start sequentially.
*
* Note: This only affects container startup, not test execution.
* Parallel test execution (@Execution(ExecutionMode.CONCURRENT)) is unsupported.
*
* @return true to start containers in parallel, false for sequential startup (default: false)
*/
boolean parallel() default false;
}Usage Examples:
// Basic usage - extension enabled, tests fail if Docker unavailable
@Testcontainers
class BasicTest {
@Container
private static GenericContainer<?> container = new GenericContainer<>("alpine:latest");
}
// Disable tests when Docker unavailable instead of failing
@Testcontainers(disabledWithoutDocker = true)
class GracefulDockerCheckTest {
@Container
private static GenericContainer<?> container = new GenericContainer<>("alpine:latest");
}
// Enable parallel container startup for faster test initialization
@Testcontainers(parallel = true)
class MultiContainerTest {
@Container
private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>();
@Container
private static MySQLContainer<?> mysql = new MySQLContainer<>();
@Container
private static MongoDBContainer mongodb = new MongoDBContainer("mongo:5.0");
// All three containers start simultaneously
}Mark container fields for automatic lifecycle management by the Testcontainers extension.
/**
* Marks fields that should be managed by the Testcontainers extension.
* Must be used in conjunction with @Testcontainers on the test class.
*
* The field must implement org.testcontainers.lifecycle.Startable interface.
* All Testcontainers container classes (GenericContainer, PostgreSQLContainer, etc.) implement this interface.
*
* Static fields: Container shared between all test methods. Started once before any test,
* stopped once after all tests complete. More efficient for expensive setup.
* Field should be final to prevent reassignment.
*
* Instance fields: Container restarted for each test method. Started before each test,
* stopped after each test. Provides complete test isolation.
*
* Field must be initialized (cannot be null).
* Field must be accessible (not cause access exceptions).
* Field type must implement Startable interface.
*
* The annotation can be applied to fields directly or used as a meta-annotation.
*/
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Container {
}Usage Examples:
@Testcontainers
class ContainerLifecycleTest {
// Shared container - started once, used by all tests
@Container
private static final PostgreSQLContainer<?> sharedPostgres =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
// Restarted container - fresh instance for each test method
@Container
private RedisContainer redis = new RedisContainer("redis:7-alpine");
@Test
void firstTest() {
// sharedPostgres: already started, shared state
// redis: started fresh for this test
}
@Test
void secondTest() {
// sharedPostgres: same instance as firstTest
// redis: new instance, previous one was stopped
}
}Inheritance Example:
@Testcontainers
abstract class BaseIntegrationTest {
@Container
protected static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine");
}
// Automatically inherits @Testcontainers and its containers
class UserRepositoryTest extends BaseIntegrationTest {
@Test
void testUserRepository() {
// postgres container available from base class
String jdbcUrl = postgres.getJdbcUrl();
}
}Nested Test Classes:
@Testcontainers
class OuterTest {
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine");
@Test
void outerTest() {
// Use postgres
}
@Nested
class InnerTest {
// Cannot have static fields in nested classes
// Must use instance container or access outer class static container
@Container
private MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@Test
void innerTest() {
// postgres (from outer) and mysql (from inner) available
String postgresUrl = postgres.getJdbcUrl();
String mysqlUrl = mysql.getJdbcUrl();
}
}
}Enable tests conditionally based on Docker availability, providing finer-grained control than the disabledWithoutDocker attribute.
/**
* Enables tests only if Docker is available.
* Tests are skipped (disabled) when Docker cannot be accessed.
*
* Can be applied at class level (affects all test methods) or method level (affects single test).
* Provides more granular control than @Testcontainers(disabledWithoutDocker = true).
*
* When applied at class level, all test methods in the class are skipped if Docker unavailable.
* When applied at method level, only that specific test is skipped if Docker unavailable.
*
* Can be combined with @Testcontainers for extra safety.
* Can be used independently without @Testcontainers for manual container management.
*
* The condition is evaluated before test execution.
* Skipped tests are reported as disabled, not failed.
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ExtendWith(EnabledIfDockerAvailableCondition.class)
public @interface EnabledIfDockerAvailable {
}Usage Examples:
// Class-level: All tests in class require Docker
@EnabledIfDockerAvailable
class DockerRequiredTest {
@Test
void testOne() {
// Skipped if Docker unavailable
}
@Test
void testTwo() {
// Skipped if Docker unavailable
}
}
// Method-level: Only specific tests require Docker
class MixedTest {
@Test
void nonDockerTest() {
// Always runs, no Docker needed
}
@Test
@EnabledIfDockerAvailable
void dockerTest() {
// Only runs if Docker available
GenericContainer<?> container = new GenericContainer<>("alpine:latest");
container.start();
// Manual cleanup required when not using @Container
container.stop();
}
}
// Combining with @Testcontainers for extra safety
@Testcontainers
@EnabledIfDockerAvailable
class SafeDockerTest {
@Container
private static GenericContainer<?> container = new GenericContainer<>("alpine:latest");
@Test
void test() {
// Tests are disabled if Docker unavailable
}
}Difference from @Testcontainers(disabledWithoutDocker = true):
@Testcontainers(disabledWithoutDocker = true): Applies to entire class, only works with @Testcontainers@EnabledIfDockerAvailable: Can be applied at class or method level, works with or without @Testcontainers// Option 1: Using @Testcontainers attribute
@Testcontainers(disabledWithoutDocker = true)
class Option1 {
@Container
private static GenericContainer<?> container = new GenericContainer<>("alpine:latest");
// All tests disabled if Docker unavailable
}
// Option 2: Using @EnabledIfDockerAvailable
@EnabledIfDockerAvailable
class Option2 {
// More explicit, can be used without @Testcontainers
// Can be applied to individual methods
@Test
void dockerTest() {
GenericContainer<?> container = new GenericContainer<>("alpine:latest");
container.start();
container.stop();
}
}The JUnit 5 extension implementation that manages container lifecycles. This class is automatically registered via the @Testcontainers annotation and should not be referenced directly by users.
/**
* JUnit Jupiter extension that provides automatic container lifecycle management.
* Registered automatically via @Testcontainers annotation.
*
* This class implements JUnit 5 extension callbacks to manage container lifecycles:
* - BeforeAllCallback: Starts static @Container fields (shared containers)
* - AfterAllCallback: Stops shared containers after all tests complete
* - BeforeEachCallback: Starts instance @Container fields (per-test containers)
* - AfterEachCallback: Stops per-test containers after each test
* - ExecutionCondition: Checks Docker availability if disabledWithoutDocker is set
*
* Extension execution order:
* - BeforeAllCallback: Executes early in the extension chain
* - BeforeEachCallback: Executes early in the extension chain
* - AfterEachCallback: Executes late in the extension chain
* - AfterAllCallback: Executes late in the extension chain
* - ExecutionCondition: Executes to determine test execution eligibility
*
* This class has no public methods for direct use.
* It is an internal implementation detail that is automatically registered when using @Testcontainers.
* Users interact with the extension through the @Testcontainers and @Container annotations.
*
* Extension validates container fields at class loading time.
* Throws ExtensionConfigurationException for invalid configurations.
*/
public class TestcontainersExtension
implements BeforeEachCallback, BeforeAllCallback,
AfterEachCallback, AfterAllCallback,
ExecutionCondition {
}Note: This class has no public methods for direct use. It is an internal implementation detail that is automatically registered when using @Testcontainers. Users interact with the extension through the @Testcontainers and @Container annotations.
All fields marked with @Container must implement the org.testcontainers.lifecycle.Startable interface. This interface is implemented by all Testcontainers container classes (e.g., GenericContainer, PostgreSQLContainer, MySQLContainer, etc.).
// Valid - implements Startable
@Container
private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>();
// Valid - GenericContainer implements Startable
@Container
private static GenericContainer<?> generic = new GenericContainer<>("alpine:latest");
// Invalid - String does not implement Startable
// @Container // ExtensionConfigurationException: "FieldName: invalidField does not implement Startable"
// private static String notAContainer = "invalid";
// Invalid - null container
// @Container // ExtensionConfigurationException: "Container containerField needs to be initialized"
// private static GenericContainer<?> nullContainer = null; // Must be initialized
// Invalid - field not accessible
// @Container
// private final GenericContainer<?> container = new GenericContainer<>("alpine:latest");
// If field access fails: ExtensionConfigurationException: "Can not access container defined in field container"Containers implementing org.testcontainers.lifecycle.TestLifecycleAware receive lifecycle callbacks from the extension:
public interface TestLifecycleAware {
/**
* Called before test execution starts.
* Invoked for each test method, even if container is static (shared).
*
* @param description Test identification information (test class, method name, display name)
*/
void beforeTest(TestDescription description);
/**
* Called after test execution completes.
* Invoked for each test method, even if container is static (shared).
* Called regardless of test outcome (success, failure, or exception).
*
* @param description Test identification information
* @param throwable Exception thrown during test execution, if any (empty if test passed)
*/
void afterTest(TestDescription description, Optional<Throwable> throwable);
}Example Usage:
class CustomContainer extends GenericContainer<CustomContainer>
implements TestLifecycleAware {
public CustomContainer() {
super("custom-image:latest");
}
@Override
public void beforeTest(TestDescription description) {
// Custom setup before each test
logger.info("Starting test: " + description.getFilesystemFriendlyName());
// Can access container state, configure logging, etc.
}
@Override
public void afterTest(TestDescription description, Optional<Throwable> throwable) {
// Custom cleanup/logging after each test
if (throwable.isPresent()) {
logger.error("Test failed: " + description.getFilesystemFriendlyName(),
throwable.get());
}
// Can perform cleanup, collect metrics, etc.
}
}
@Testcontainers
class CustomContainerTest {
@Container
private static CustomContainer container = new CustomContainer();
@Test
void test() {
// beforeTest() called automatically before this test
// afterTest() called automatically after this test
}
}Important Notes:
beforeTest() is called for each test method, even for static (shared) containersafterTest() is called for each test method, even for static (shared) containersbeforeTest() throws an exception, the test is marked as failedafterTest() throws an exception, it is logged but does not affect test outcomeStatic fields with @Container are started once before any test method and stopped once after all tests complete. This is more efficient for expensive container setup but means tests share container state.
@Testcontainers
class SharedContainerTest {
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb");
@Test
void firstTest() {
// postgres started before this test (if first test to run)
// Insert data into database
}
@Test
void secondTest() {
// Same postgres instance as firstTest
// Data from firstTest may still be present
// postgres will be stopped after this test (if last test to run)
}
}Best Practices for Shared Containers:
final to prevent accidental reassignmentInteraction with JUnit 5 Lifecycle:
@BeforeAll phase (before any @BeforeAll methods)@AfterAll phase (after all @AfterAll methods)@BeforeAll methods can safely access static containers@AfterAll methods can safely access static containers before they are stoppedInstance fields with @Container are started fresh before each test method and stopped after each test completes. This provides complete test isolation but is less efficient.
@Testcontainers
class RestartedContainerTest {
@Container
private PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb");
@Test
void firstTest() {
// Fresh postgres instance started for this test
// Insert data into database
// postgres stopped after this test
}
@Test
void secondTest() {
// New postgres instance started for this test
// Database is empty - no data from firstTest
// postgres stopped after this test
}
}Best Practices for Restarted Containers:
Interaction with JUnit 5 Lifecycle:
@BeforeEach phase (before any @BeforeEach methods)@AfterEach phase (after all @AfterEach methods)@BeforeEach methods can safely access instance containers@AfterEach methods can safely access instance containers before they are stoppedYou can combine both lifecycle modes in the same test class:
@Testcontainers
class MixedLifecycleTest {
// Shared - expensive database with read-only reference data
@Container
private static final PostgreSQLContainer<?> referenceDb =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("reference_data");
// Restarted - working database that each test modifies
@Container
private PostgreSQLContainer<?> workingDb =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("working");
@Test
void firstTest() {
// referenceDb: shared, contains read-only data
// workingDb: fresh instance for this test
}
@Test
void secondTest() {
// referenceDb: same instance as firstTest
// workingDb: new fresh instance
}
}Enable parallel container startup to reduce test initialization time when using multiple containers:
@Testcontainers(parallel = true)
class ParallelStartupTest {
@Container
private static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine");
@Container
private static MySQLContainer<?> mysql =
new MySQLContainer<>("mysql:8.0");
@Container
private static MongoDBContainer mongodb =
new MongoDBContainer("mongo:5.0");
@Container
private static final Neo4jContainer<?> neo4j =
new Neo4jContainer<>("neo4j:5");
// All four containers start simultaneously instead of sequentially
// Reduces total startup time significantly
}Important Notes:
When to Use Parallel Startup:
When NOT to Use Parallel Startup:
The extension throws ExtensionConfigurationException in the following cases:
// Error: Field does not implement Startable
@Testcontainers
class InvalidTypeTest {
@Container // ExtensionConfigurationException: "FieldName: invalidField does not implement Startable"
private String invalidField = "not a container";
}
// Error: Container field is null
@Testcontainers
class NullContainerTest {
@Container // ExtensionConfigurationException: "Container containerField needs to be initialized"
private static GenericContainer<?> containerField; // null - never initialized
}
// Error: Cannot access field
@Testcontainers
class InaccessibleFieldTest {
@Container
private final GenericContainer<?> container = new GenericContainer<>("alpine:latest");
// If field access fails: ExtensionConfigurationException: "Can not access container defined in field container"
}
// Error: Extension used on non-class
// ExtensionConfigurationException: "TestcontainersExtension is only supported for classes."
// This occurs if @Testcontainers is used on a method or field instead of a classContainer-Level Exceptions:
ContainerLaunchException: Thrown by containers if startup fails (Docker daemon unavailable, image pull failure, health check timeout, port conflicts)IllegalStateException: Thrown by containers if methods are called in wrong order (e.g., getJdbcUrl() before start())TimeoutException: Thrown if container startup exceeds timeoutDocker Unavailability Handling:
disabledWithoutDocker = true: Tests are skipped (disabled) if Docker unavailable@EnabledIfDockerAvailable: Tests are skipped if Docker unavailableDo NOT use @Container with manually managed containers:
// ❌ Anti-pattern: Manual lifecycle with @Container
@Testcontainers
class BadPattern {
@Container // Extension will try to start/stop, conflicts with manual management
private static PostgreSQLContainer<?> postgres = SharedPostgresContainer.getInstance();
// Container already started manually, extension will cause conflicts
}
// ✅ Correct: Manual lifecycle without @Container
@Testcontainers
class GoodPattern {
// No @Container annotation - manual lifecycle
private static PostgreSQLContainer<?> postgres = SharedPostgresContainer.getInstance();
// Container managed manually, extension ignores it
}Do NOT use lazy initialization:
// ❌ Anti-pattern: Lazy initialization
@Testcontainers
class BadPattern {
@Container
private static PostgreSQLContainer<?> postgres; // null initially
@BeforeAll
static void setup() {
postgres = new PostgreSQLContainer<>("postgres:15-alpine");
// Extension validates at class loading time, before this runs
// Will throw ExtensionConfigurationException
}
}
// ✅ Correct: Initialize in field declaration
@Testcontainers
class GoodPattern {
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine");
// Container initialized when field is declared
}Do NOT use @Container on non-Startable types:
// ❌ Anti-pattern: Wrong type
@Testcontainers
class BadPattern {
@Container // ExtensionConfigurationException
private static String notAContainer = "invalid";
}
// ✅ Correct: Use container type
@Testcontainers
class GoodPattern {
@Container
private static GenericContainer<?> container = new GenericContainer<>("alpine:latest");
}Do NOT configure containers after extension starts them:
// ❌ Anti-pattern: Configuration after start
@Testcontainers
class BadPattern {
@Container
private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>();
@BeforeAll
static void setup() {
// Too late - extension already started container
postgres.withDatabaseName("testdb"); // May not take effect
}
}
// ✅ Correct: Configure in field initializer
@Testcontainers
class GoodPattern {
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
// All configuration set before extension starts container
}The junit-jupiter module requires:
The core Testcontainers library provides:
org.testcontainers.lifecycle.Startable - Required interface for all containersorg.testcontainers.lifecycle.TestLifecycleAware - Optional interface for lifecycle callbacksorg.testcontainers.containers.GenericContainer - Base container classNote: Since the core Testcontainers library depends on JUnit 4.x, projects using this module will have both JUnit Jupiter and JUnit 4.x on the test classpath. This is generally not an issue, but be aware when managing dependencies.
For very expensive containers that should be shared across all test classes, use the singleton pattern:
public class SharedPostgresContainer {
private static final PostgreSQLContainer<?> INSTANCE =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("integration_tests");
static {
INSTANCE.start();
Runtime.getRuntime().addShutdownHook(new Thread(INSTANCE::stop));
}
public static PostgreSQLContainer<?> getInstance() {
return INSTANCE;
}
}
@Testcontainers
class FirstTest {
// Do NOT use @Container with singleton pattern
private static final PostgreSQLContainer<?> postgres =
SharedPostgresContainer.getInstance();
@Test
void test() {
// Use shared postgres instance
String jdbcUrl = postgres.getJdbcUrl();
}
}
@Testcontainers
class SecondTest {
private static final PostgreSQLContainer<?> postgres =
SharedPostgresContainer.getInstance();
@Test
void test() {
// Use same postgres instance as FirstTest
}
}Important: When using singleton pattern, do not use @Container annotation as you're managing lifecycle manually. The extension will try to start/stop the container, which conflicts with manual management.
Use the extension with JUnit 5 parameterized tests:
@Testcontainers
class ParameterizedContainerTest {
@Container
private static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine");
@ParameterizedTest
@ValueSource(strings = {"user1", "user2", "user3"})
void testMultipleUsers(String username) {
// Container is started once, used for all parameter iterations
// Test runs for each username value
// Container is shared across all parameter iterations
}
}Lifecycle for Parameterized Tests:
Use the extension with JUnit 5 repeated tests:
@Testcontainers
class RepeatedContainerTest {
@Container
private static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine");
@Container
private RedisContainer redis = new RedisContainer("redis:7-alpine");
@RepeatedTest(5)
void testWithRepetition() {
// postgres: shared across all repetitions (static)
// redis: restarted for each repetition (instance)
}
}Lifecycle for Repeated Tests:
Use the extension with JUnit 5 test factories:
@Testcontainers
class DynamicContainerTest {
@Container
private static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine");
@Container
private RedisContainer redis = new RedisContainer("redis:7-alpine");
@TestFactory
Stream<DynamicTest> dynamicTests() {
return Stream.of("test1", "test2", "test3")
.map(name -> DynamicTest.dynamicTest("Test: " + name, () -> {
// postgres: shared across all dynamic tests (static)
// redis: restarted for each dynamic test (instance)
}));
}
}Lifecycle for Dynamic Tests:
Configure containers before they are started:
@Testcontainers
class CustomConfigTest {
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("customdb")
.withUsername("testuser")
.withPassword("testpass")
.withInitScript("init-schema.sql")
.withEnv("POSTGRES_INITDB_ARGS", "--encoding=UTF-8")
.withCommand("postgres", "-c", "log_statement=all");
@Test
void test() {
// Container starts with custom configuration
// All configuration must be set in field initializer
}
}Configuration Rules:
The extension integrates with JUnit 5's lifecycle callbacks:
@Testcontainers
class LifecycleIntegrationTest {
@Container
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine");
@BeforeAll
static void beforeAll() {
// postgres is already started at this point
// Can safely access postgres.getJdbcUrl(), etc.
}
@BeforeEach
void beforeEach() {
// postgres is still running (static container)
// Can safely access postgres
}
@Test
void test() {
// postgres is available
}
@AfterEach
void afterEach() {
// postgres is still running (static container)
// Can safely access postgres
}
@AfterAll
static void afterAll() {
// postgres is still running at this point
// Will be stopped after this method completes
// Can safely access postgres
}
}Execution Order:
BeforeAllCallback: Start static containers@BeforeAll methodsBeforeEachCallback: Start instance containers@BeforeEach methods@AfterEach methodsAfterEachCallback: Stop instance containers@AfterAll methodsAfterAllCallback: Stop static containersSequential Test Execution Only: The extension has only been tested with sequential test execution. Using it with JUnit 5 parallel test execution (@Execution(ExecutionMode.CONCURRENT)) is unsupported and may have unintended side effects. Container lifecycle management is not thread-safe for concurrent test execution.
Static Fields in Nested Classes: Shared containers cannot be declared as static fields inside nested test classes (classes annotated with @Nested). This is because nested test classes must be non-static and cannot have static fields. Use shared containers in the outer class instead, or use instance containers in nested classes.
JUnit 4.x Also Present: Since the core Testcontainers library depends on JUnit 4.x, projects using this module will have both JUnit Jupiter and JUnit 4.x on the test classpath. This is generally not an issue, but be aware when managing dependencies.
Manual Lifecycle Control: When using singleton pattern or manual lifecycle control, do not use @Container annotation as it conflicts with manual management. The extension will try to start/stop containers that are already managed manually.
Container Reuse: When container reuse is enabled (testcontainers.reuse.enable=true), reused containers are not stopped by the extension. The extension will start containers but won't stop them if they're reused. This is by design to allow containers to persist across test runs.
Extension Ordering: The extension executes early in the extension chain for BeforeAllCallback and BeforeEachCallback, and late for AfterAllCallback and AfterEachCallback. This ordering cannot be customized. If you need different ordering, consider manual container management.
Field Initialization: Container fields must be initialized in the field declaration. Lazy initialization or initialization in @BeforeAll methods is not supported. The extension discovers and validates containers at class loading time.
// Pattern 1: Simple shared container
@Testcontainers
class Pattern1 {
@Container
private static GenericContainer<?> container = new GenericContainer<>("image:tag");
}
// Pattern 2: Restarted containers for isolation
@Testcontainers
class Pattern2 {
@Container
private GenericContainer<?> container = new GenericContainer<>("image:tag");
}
// Pattern 3: Graceful Docker absence handling
@Testcontainers(disabledWithoutDocker = true)
class Pattern3 {
@Container
private static GenericContainer<?> container = new GenericContainer<>("image:tag");
}
// Pattern 4: Parallel container startup
@Testcontainers(parallel = true)
class Pattern4 {
@Container
private static PostgreSQLContainer<?> db1 = new PostgreSQLContainer<>();
@Container
private static MySQLContainer<?> db2 = new MySQLContainer<>();
}
// Pattern 5: Mixed lifecycle
@Testcontainers
class Pattern5 {
@Container
private static GenericContainer<?> shared = new GenericContainer<>("shared:tag");
@Container
private GenericContainer<?> perTest = new GenericContainer<>("pertest:tag");
}
// Pattern 6: Test inheritance
@Testcontainers
abstract class Pattern6Base {
@Container
protected static GenericContainer<?> container = new GenericContainer<>("image:tag");
}
class Pattern6Child extends Pattern6Base {
@Test void test() { /* container available */ }
}
// Pattern 7: Conditional execution by method
class Pattern7 {
@Test
@EnabledIfDockerAvailable
void dockerRequiredTest() { /* runs only if Docker available */ }
}
// Pattern 8: Nested test classes
@Testcontainers
class Pattern8 {
@Container
private static GenericContainer<?> outer = new GenericContainer<>("outer:tag");
@Nested
class Inner {
@Container
private GenericContainer<?> inner = new GenericContainer<>("inner:tag");
}
}
// Pattern 9: Parameterized tests
@Testcontainers
class Pattern9 {
@Container
private static GenericContainer<?> container = new GenericContainer<>("image:tag");
@ParameterizedTest
@ValueSource(strings = {"a", "b", "c"})
void test(String param) { /* container shared across iterations */ }
}
// Pattern 10: Custom container with lifecycle callbacks
@Testcontainers
class Pattern10 {
@Container
private static CustomContainer container = new CustomContainer();
// CustomContainer implements TestLifecycleAware
}