or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.md
tile.json

index.mddocs/

Testcontainers JUnit Jupiter Extension

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.

Key Information for Agents

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)
  • Docker daemon must be running and accessible
  • Java 8 or higher

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 availability
  • TestcontainersExtension - JUnit 5 extension implementation (automatically registered via @Testcontainers)
  • Automatic container startup/stop based on field modifiers (static = shared, instance = per-test)
  • Support for parallel container startup within a test class
  • Integration with TestLifecycleAware interface for container-level test callbacks
  • Graceful handling of Docker unavailability (optional test disabling)

Key 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, ExecutionCondition
  • All container fields must implement org.testcontainers.lifecycle.Startable
  • Containers may optionally implement org.testcontainers.lifecycle.TestLifecycleAware for callbacks

Default Behaviors:

  • Static @Container fields: Shared across all test methods in class (started once before first test, stopped after last test)
  • Instance @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)
  • Extension executes early in BeforeAllCallback/BeforeEachCallback chain, late in AfterAllCallback/AfterEachCallback chain
  • Container fields must be initialized (cannot be null) in field declaration
  • Extension validates container fields at class loading time
  • @Testcontainers annotation is inherited by subclasses (@Inherited)
  • Tests are disabled (not failed) when Docker unavailable if disabledWithoutDocker = true or @EnabledIfDockerAvailable is used
  • Container configuration (e.g., withDatabaseName(), withEnv()) must be set before extension starts container (in field initializer)

Threading Model:

  • Container lifecycle operations (start/stop) are synchronous and blocking
  • Static containers are started once before all tests in a class (in @BeforeAll phase)
  • Instance containers are started before each test method (in @BeforeEach phase)
  • Container access methods (e.g., getJdbcUrl(), getMappedPort()) are thread-safe for read operations once container is started
  • Container configuration methods must be called before the extension starts the container
  • Extension callbacks execute in JUnit 5's test execution thread
  • Parallel container startup (parallel = true) starts multiple containers concurrently but does not affect test execution order
  • Extension has only been tested with sequential test execution; parallel test execution (@Execution(ExecutionMode.CONCURRENT)) is unsupported

Lifecycle:

  • Static @Container fields: Started once before any test method in the class (via BeforeAllCallback), stopped once after all tests complete (via AfterAllCallback)
  • Instance @Container fields: Started before each test method (via BeforeEachCallback), stopped after each test completes (via AfterEachCallback)
  • Containers implementing TestLifecycleAware receive beforeTest() callback before each test and afterTest() callback after each test
  • Container configuration (e.g., withDatabaseName(), withEnv()) must be set before the extension starts the container (typically in field initializer)
  • Containers are automatically stopped even if tests fail or throw exceptions
  • Extension validates container fields at class loading time and throws ExtensionConfigurationException for invalid configurations
  • When disabledWithoutDocker = true, tests are disabled (not failed) if Docker is unavailable
  • When @EnabledIfDockerAvailable is used, tests are skipped if Docker is unavailable
  • Extension is inherited by subclasses (annotation is @Inherited)
  • Nested test classes (@Nested) can have their own instance containers; static containers from outer class are accessible

Common Patterns:

  • Shared Container Pattern: Use static @Container field for expensive containers shared across tests
  • Isolated Container Pattern: Use instance @Container field for complete test isolation
  • Mixed Lifecycle Pattern: Combine static (shared) and instance (per-test) containers in same class
  • Graceful Docker Handling: Use @Testcontainers(disabledWithoutDocker = true) or @EnabledIfDockerAvailable to skip tests when Docker unavailable
  • Parallel Startup Pattern: Use @Testcontainers(parallel = true) for multiple independent containers
  • Test Inheritance Pattern: Use @Testcontainers on base class, containers inherited by subclasses
  • Nested Test Pattern: Use instance containers in @Nested classes, access outer static containers
  • Parameterized Test Pattern: Static containers shared across parameter iterations, instance containers restarted per iteration
  • Custom Lifecycle Pattern: Implement TestLifecycleAware for container-level test callbacks

Integration Points:

  • Works with all JUnit 5 test types: @Test, @ParameterizedTest, @RepeatedTest, @TestFactory, @TestTemplate
  • Integrates with JUnit 5 lifecycle callbacks: @BeforeAll, @BeforeEach, @AfterEach, @AfterAll (containers available in all callbacks)
  • Compatible with JUnit 5 nested tests (@Nested) - static containers from outer class accessible, instance containers in nested class
  • Works with test inheritance - @Testcontainers annotation inherited, containers from base class available
  • Extension ordering: Executes early for BeforeAllCallback/BeforeEachCallback, late for AfterAllCallback/AfterEachCallback
  • Compatible with container reuse (testcontainers.reuse.enable=true) - reused containers not stopped by extension
  • Requires containers implementing org.testcontainers.lifecycle.Startable (all Testcontainers container classes)
  • Optional integration with org.testcontainers.lifecycle.TestLifecycleAware for test-level callbacks

Critical Edge Cases:

  • Static fields in nested classes: Nested test classes cannot have static fields, so shared containers must be in outer class
  • Test inheritance: @Testcontainers annotation is inherited; containers in base class are available to subclasses
  • Parameterized tests: Containers are started once and reused for all parameter iterations (static) or restarted per iteration (instance)
  • Repeated tests (@RepeatedTest): Instance containers are restarted for each repetition; static containers are shared
  • Test factories (@TestFactory): Containers follow same lifecycle rules as regular tests
  • Dynamic tests (@TestFactory returning DynamicTest): Instance containers are restarted for each dynamic test; static containers are shared
  • Test templates (@TestTemplate): Containers follow same lifecycle rules as regular tests
  • Extension ordering: TestcontainersExtension executes before most other extensions; containers are available in @BeforeAll and @BeforeEach callbacks
  • Manual container management: Do not use @Container with manually managed containers (singleton pattern, try-with-resources)
  • Container reuse: Extension works with container reuse, but reused containers are not stopped by extension
  • Parallel test execution: Unsupported - extension only tested with sequential test execution
  • Field initialization: Container fields must be initialized in field declaration; lazy initialization not supported
  • Null containers: Extension throws ExtensionConfigurationException if container field is null

Exception Handling:

  • ExtensionConfigurationException: Thrown when container field configuration is invalid:
    • Field does not implement Startable interface
    • Container field is null (not initialized)
    • Cannot access container field (accessibility issues)
    • Extension used on non-class element
  • ContainerLaunchException: 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())
  • Tests are disabled (not failed) when Docker unavailable if disabledWithoutDocker = true or @EnabledIfDockerAvailable is used
  • Container exceptions during startup cause test to fail immediately
  • Container exceptions during stop are logged but do not affect test outcome

Decision Guidance:

  • Use static containers when: Container startup is expensive, tests are read-only, test isolation not critical, need shared state
  • Use instance containers when: Tests modify container state, need complete isolation, container startup is fast, test data conflicts would cause issues
  • Use parallel = true when: Multiple independent containers, container startup time is bottleneck, containers have no dependencies
  • Use disabledWithoutDocker = true when: Tests should be skipped (not failed) if Docker unavailable, want graceful degradation
  • Use @EnabledIfDockerAvailable when: Need method-level control, want explicit Docker requirement, using without @Testcontainers
  • Use TestLifecycleAware when: Need container-level test callbacks, want to track test execution, need custom logging/metrics

Package Information

  • Package Name: junit-jupiter
  • Package Type: Maven
  • Group ID: org.testcontainers
  • Artifact ID: junit-jupiter
  • Version: 1.21.4
  • Language: Java
  • License: MIT
  • Installation:

Maven:

<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'

Core Imports

import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.EnabledIfDockerAvailable;

Basic Usage

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
    }
}

Architecture

The JUnit Jupiter extension provides a declarative container management model:

  • Extension Registration: The @Testcontainers annotation automatically registers the TestcontainersExtension with JUnit 5
  • Container Discovery: The extension scans test classes for fields annotated with @Container
  • Lifecycle Management: Containers are started/stopped based on field modifiers (static vs instance)
  • Test Callbacks: Containers implementing TestLifecycleAware receive test lifecycle notifications
  • Conditional Execution: Optional Docker availability checking for graceful test handling
  • Extension Callbacks: Implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, and ExecutionCondition from JUnit 5

Capabilities

Extension Activation

Activate 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
}

Container Field Marking

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();
        }
    }
}

Docker Availability Check

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();
    }
}

Extension Class

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.

Container Requirements

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"

TestLifecycleAware Integration

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) containers
  • afterTest() is called for each test method, even for static (shared) containers
  • Callbacks are invoked in the test execution thread
  • If beforeTest() throws an exception, the test is marked as failed
  • If afterTest() throws an exception, it is logged but does not affect test outcome

Lifecycle Modes

Shared Containers (Static Fields)

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

  • Use for read-only resources or when test isolation isn't critical
  • Clear state between tests manually if needed (e.g., truncate tables, clear caches)
  • Most efficient for expensive containers (databases with large init scripts, browsers, etc.)
  • Mark field as final to prevent accidental reassignment
  • Use when container startup time is significant compared to test execution time

Interaction with JUnit 5 Lifecycle:

  • Static containers are started in @BeforeAll phase (before any @BeforeAll methods)
  • Static containers are stopped in @AfterAll phase (after all @AfterAll methods)
  • @BeforeAll methods can safely access static containers
  • @AfterAll methods can safely access static containers before they are stopped

Restarted Containers (Instance Fields)

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

  • Use when tests modify container state and need isolation
  • Accept the performance overhead of multiple container starts
  • Ideal for smaller, faster-starting containers
  • Use when test data conflicts would cause issues
  • Use when tests need a clean slate for each execution

Interaction with JUnit 5 Lifecycle:

  • Instance containers are started in @BeforeEach phase (before any @BeforeEach methods)
  • Instance containers are stopped in @AfterEach phase (after all @AfterEach methods)
  • @BeforeEach methods can safely access instance containers
  • @AfterEach methods can safely access instance containers before they are stopped

Mixed Lifecycle

You 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
    }
}

Parallel Container Startup

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:

  • Parallel startup is only for container initialization, not test execution
  • Containers start concurrently but tests still execute sequentially (unless using JUnit 5 parallel execution, which is unsupported)
  • Use when you have multiple independent containers that can start simultaneously
  • Container startup order is not guaranteed when using parallel mode
  • If containers have dependencies, use sequential startup (default)
  • Parallel startup does not affect container stop order (containers stop in reverse order of start)

When to Use Parallel Startup:

  • Multiple independent containers (e.g., different databases, message brokers)
  • Container startup time is a bottleneck
  • Containers don't have startup dependencies
  • Test class has many containers

When NOT to Use Parallel Startup:

  • Containers have dependencies (e.g., app container depends on database container)
  • Container startup is fast and overhead is negligible
  • Need deterministic startup order

Error Handling

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 class

Container-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 timeout

Docker Unavailability Handling:

  • Default behavior: Tests fail with exception if Docker unavailable
  • With disabledWithoutDocker = true: Tests are skipped (disabled) if Docker unavailable
  • With @EnabledIfDockerAvailable: Tests are skipped if Docker unavailable
  • Skipped tests are reported as disabled, not failed

Anti-Patterns

Do 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
}

Dependencies

The junit-jupiter module requires:

  • org.testcontainers:testcontainers (core library) - Provides container implementations
  • org.junit.jupiter:junit-jupiter-api - JUnit 5 API
  • Java 8 or higher

The core Testcontainers library provides:

  • org.testcontainers.lifecycle.Startable - Required interface for all containers
  • org.testcontainers.lifecycle.TestLifecycleAware - Optional interface for lifecycle callbacks
  • org.testcontainers.containers.GenericContainer - Base container class
  • Specialized container classes (PostgreSQLContainer, MySQLContainer, etc.)

Note: 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.

Advanced Patterns

Singleton Containers

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.

Parameterized Tests

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:

  • Static containers: Started once before first parameter iteration, stopped after last iteration
  • Instance containers: Started before each parameter iteration, stopped after each iteration

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

  • Static containers: Started once before first repetition, stopped after last repetition
  • Instance containers: Started before each repetition, stopped after each repetition

Test Factories and Dynamic 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:

  • Static containers: Started once before first dynamic test, stopped after last dynamic test
  • Instance containers: Started before each dynamic test, stopped after each dynamic test

Custom Container Configuration

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:

  • All container configuration must be set in the field initializer
  • Configuration cannot be changed after the extension starts the container
  • Use method chaining for multiple configuration calls
  • Configuration is applied before container startup

Integration with JUnit 5 Lifecycle Callbacks

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:

  1. Extension BeforeAllCallback: Start static containers
  2. @BeforeAll methods
  3. For each test:
    • Extension BeforeEachCallback: Start instance containers
    • @BeforeEach methods
    • Test method
    • @AfterEach methods
    • Extension AfterEachCallback: Stop instance containers
  4. @AfterAll methods
  5. Extension AfterAllCallback: Stop static containers

Limitations

  1. Sequential 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

Common Patterns Summary

// 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
}