or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

configuration.mdcontainer-lifecycle.mddocker-client.mddocker-compose.mdimage-building.mdimage-management.mdimage-pull-policies.mdindex.mdjunit-jupiter-integration.mdlifecycle.mdmodules-overview.mdnetwork-configuration.mdoutput-handling.mdstartup-checks.mdutility-classes.mdwait-strategies.md
tile.json

junit-jupiter-integration.mddocs/

JUnit Jupiter Integration

Testcontainers provides seamless integration with JUnit Jupiter (JUnit 5) through a dedicated extension. The extension automatically manages container lifecycle, starting containers before tests and stopping them after tests complete, with support for both shared and per-test container instances.

Overview

The JUnit Jupiter integration provides three main annotations:

  • @Testcontainers: Enables automatic container lifecycle management for test classes
  • @Container: Marks container fields to be managed by the extension
  • @EnabledIfDockerAvailable: Conditionally enables tests based on Docker availability

Container lifecycle depends on field modifiers:

  • Static fields (static): Containers shared across all test methods (started once, stopped after all tests)
  • Instance fields (non-static): Containers restarted for each test method

Package Information

Module: org.testcontainers:testcontainers-junit-jupiter

Maven Dependency:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>2.0.3</version>
    <scope>test</scope>
</dependency>

Gradle Dependency:

testImplementation 'org.testcontainers:junit-jupiter:2.0.3'

Core Annotations

@Testcontainers Annotation

Activates automatic container management for a test class. The extension finds all @Container fields and manages their lifecycle.

import org.testcontainers.junit.jupiter.Testcontainers;

/**
 * JUnit Jupiter extension to activate automatic startup and stop of containers.
 * Finds all fields annotated with @Container and calls their lifecycle methods.
 *
 * Static @Container fields are shared between test methods.
 * Instance @Container fields are restarted for every test method.
 *
 * Can be used on superclasses - subclasses automatically inherit the extension.
 *
 * WARNING: Only tested with sequential test execution.
 * Parallel test execution is unsupported and may have unintended side effects.
 */
@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 if Docker unavailable.
     * When false (default), tests fail if Docker unavailable.
     *
     * @return if the tests should be disabled when Docker is not available
     */
    boolean disabledWithoutDocker() default false;

    /**
     * Whether containers should start in parallel.
     * When true, all @Container fields start concurrently using Startables.deepStart().
     * When false (default), containers start sequentially.
     *
     * Parallel startup can significantly reduce test suite execution time
     * when multiple containers are used.
     *
     * @return if the containers should start in parallel
     */
    boolean parallel() default false;
}

Key Features:

  • @Inherited: Subclasses automatically inherit container management
  • @ExtendWith(TestcontainersExtension.class): Automatically registers the JUnit 5 extension
  • Supports test class hierarchies

Usage Examples:

import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.postgresql.PostgreSQLContainer;
import org.junit.jupiter.api.Test;

// Basic usage
@Testcontainers
class BasicContainerTest {
    @Container
    private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    @Test
    void test() {
        // postgres container is running
        assertTrue(postgres.isRunning());
    }
}

// Disable tests if Docker not available
@Testcontainers(disabledWithoutDocker = true)
class GracefulSkipTest {
    @Container
    private static GenericContainer<?> container = new GenericContainer<>("redis:7.0");

    @Test
    void test() {
        // Test is skipped (not failed) if Docker unavailable
    }
}

// Parallel container startup
@Testcontainers(parallel = true)
class ParallelStartupTest {
    @Container
    private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    @Container
    private static GenericContainer<?> redis = new GenericContainer<>("redis:7.0");

    @Test
    void test() {
        // Both containers started in parallel (faster than sequential)
        assertTrue(postgres.isRunning());
        assertTrue(redis.isRunning());
    }
}

@Container Annotation

Marks container fields to be managed by the Testcontainers extension. Must be used with @Testcontainers on the class.

import org.testcontainers.junit.jupiter.Container;

/**
 * Marks containers that should be managed by the Testcontainers extension.
 * Used in conjunction with @Testcontainers annotation.
 *
 * Can be applied to:
 * - Fields implementing the Startable interface
 * - Static fields (shared across all tests)
 * - Instance fields (restarted per test)
 * - Used as a meta-annotation for custom container annotations
 */
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Container {
    // No parameters
}

Behavior:

  • Static fields: Container started once before all tests, stopped after all tests complete
  • Instance fields: Container started before each test method, stopped after each test method
  • Fields must implement Startable interface (all Testcontainers containers do)

Usage Examples:

import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.mysql.MySQLContainer;
import org.testcontainers.postgresql.PostgreSQLContainer;

@Testcontainers
class ContainerLifecycleTest {

    // Shared container - started once, used by all tests
    @Container
    private static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");

    // Per-test container - restarted for each test method
    @Container
    private PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    private static String mysqlContainerId;

    @Test
    void firstTest() {
        // MySQL container ID stays the same across tests
        if (mysqlContainerId == null) {
            mysqlContainerId = mysql.getContainerId();
        } else {
            assertEquals(mysqlContainerId, mysql.getContainerId());
        }

        // PostgreSQL gets a new container for this test
        assertTrue(postgres.isRunning());
    }

    @Test
    void secondTest() {
        // Same MySQL container as firstTest
        assertEquals(mysqlContainerId, mysql.getContainerId());

        // Different PostgreSQL container than firstTest
        assertTrue(postgres.isRunning());
    }
}

@EnabledIfDockerAvailable Annotation

Conditionally enables tests based on Docker availability. Tests are skipped if Docker is not accessible.

import org.testcontainers.junit.jupiter.EnabledIfDockerAvailable;

/**
 * JUnit Jupiter extension to enable tests only if Docker is available.
 * Tests or test classes annotated with this annotation are skipped if Docker cannot be accessed.
 *
 * Can be applied to:
 * - Test classes (all tests in the class are conditional)
 * - Individual test methods (only that method is conditional)
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ExtendWith(EnabledIfDockerAvailableCondition.class)
public @interface EnabledIfDockerAvailable {
    // No parameters
}

Behavior:

  • Checks Docker availability before executing tests
  • If Docker unavailable, test is skipped (not failed)
  • More flexible than @Testcontainers(disabledWithoutDocker = true) as it can be applied per-method

Usage Examples:

import org.testcontainers.junit.jupiter.EnabledIfDockerAvailable;
import org.junit.jupiter.api.Test;

// Apply to entire test class
@EnabledIfDockerAvailable
class DockerRequiredTests {
    @Test
    void dockerTest1() {
        // Skipped if Docker unavailable
    }

    @Test
    void dockerTest2() {
        // Skipped if Docker unavailable
    }
}

// Apply to individual test methods
class MixedTests {
    @Test
    void regularTest() {
        // Always runs (no Docker required)
    }

    @Test
    @EnabledIfDockerAvailable
    void dockerRequiredTest() {
        // Skipped if Docker unavailable
    }
}

// Combine with @Testcontainers
@Testcontainers
@EnabledIfDockerAvailable
class SafeContainerTests {
    @Container
    private static GenericContainer<?> container = new GenericContainer<>("nginx:alpine");

    @Test
    void test() {
        // Test skipped gracefully if Docker unavailable
        assertTrue(container.isRunning());
    }
}

Container Lifecycle Patterns

Shared Container (Static Field)

Containers declared as static fields are shared across all test methods in the class.

import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.postgresql.PostgreSQLContainer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeAll;

@Testcontainers
class SharedContainerTest {

    @Container
    private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @BeforeAll
    static void setup() {
        // Container is already running
        System.out.println("JDBC URL: " + postgres.getJdbcUrl());
    }

    @Test
    void test1() {
        // Uses shared container
        assertNotNull(postgres.getJdbcUrl());
    }

    @Test
    void test2() {
        // Uses same container as test1
        assertNotNull(postgres.getJdbcUrl());
    }

    // Container stopped automatically after all tests
}

Lifecycle:

beforeAll() -> start container
test1()
test2()
test3()
...
afterAll() -> stop container

When to Use:

  • Container setup is expensive (large images, complex initialization)
  • Tests don't modify container state
  • Tests are read-only or idempotent
  • Faster test execution (container started once)

Per-Test Container (Instance Field)

Containers declared as instance fields are restarted for every test method.

import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.containers.GenericContainer;
import org.junit.jupiter.api.Test;

@Testcontainers
class PerTestContainerTest {

    @Container
    private GenericContainer<?> redis = new GenericContainer<>("redis:7.0")
        .withExposedPorts(6379);

    @Test
    void test1() {
        // Fresh Redis container for this test
        assertTrue(redis.isRunning());
        // Can modify container state without affecting other tests
    }

    @Test
    void test2() {
        // New Redis container (different from test1)
        assertTrue(redis.isRunning());
        // Starts with clean state
    }

    // Container stopped after each test
}

Lifecycle:

beforeEach() -> start container
test1()
afterEach() -> stop container

beforeEach() -> start NEW container
test2()
afterEach() -> stop container

When to Use:

  • Tests modify container state
  • Test isolation is critical
  • Fresh container state required for each test
  • Debugging (easier to trace test-specific issues)

Mixed Lifecycle Pattern

Combine static and instance containers for different use cases in the same test class.

import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.postgresql.PostgreSQLContainer;
import org.testcontainers.containers.GenericContainer;
import org.junit.jupiter.api.Test;

@Testcontainers
class MixedLifecycleTest {

    // Shared database - read-only, expensive to start
    @Container
    private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    // Per-test cache - modified by tests, needs isolation
    @Container
    private GenericContainer<?> redis = new GenericContainer<>("redis:7.0")
        .withExposedPorts(6379);

    @Test
    void test1() {
        // Shared postgres (same for all tests)
        String jdbcUrl = postgres.getJdbcUrl();

        // Unique redis (fresh for this test)
        assertTrue(redis.isRunning());
    }

    @Test
    void test2() {
        // Same postgres as test1
        String jdbcUrl = postgres.getJdbcUrl();

        // Different redis than test1
        assertTrue(redis.isRunning());
    }
}

Advanced Patterns

Test Inheritance

The @Testcontainers annotation is @Inherited, so subclasses automatically get container management.

import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.containers.GenericContainer;

@Testcontainers
abstract class AbstractContainerTest {

    @Container
    protected static GenericContainer<?> sharedContainer = new GenericContainer<>("redis:7.0")
        .withExposedPorts(6379);

    @Container
    protected GenericContainer<?> perTestContainer = new GenericContainer<>("nginx:alpine")
        .withExposedPorts(80);
}

// Inherits @Testcontainers and all containers
class ConcreteTest extends AbstractContainerTest {

    @Container
    private static GenericContainer<?> additionalSharedContainer =
        new GenericContainer<>("mongo:7");

    @Test
    void test() {
        // Access inherited shared container
        assertTrue(sharedContainer.isRunning());

        // Access inherited per-test container
        assertTrue(perTestContainer.isRunning());

        // Access this class's container
        assertTrue(additionalSharedContainer.isRunning());
    }
}

Nested Test Classes

Nested test classes can access parent containers.

import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.containers.GenericContainer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@Testcontainers
class NestedContainerTest {

    @Container
    private static final GenericContainer<?> parentContainer =
        new GenericContainer<>("redis:7.0");

    @Test
    void parentTest() {
        assertTrue(parentContainer.isRunning());
    }

    @Nested
    class NestedTests {

        @Test
        void nestedTest() {
            // Can access parent container
            assertTrue(parentContainer.isRunning());
        }

        @Test
        void anotherNestedTest() {
            // Same parent container instance
            assertTrue(parentContainer.isRunning());
        }
    }

    @Nested
    class AnotherNestedGroup {

        @Test
        void test() {
            // Shares parent container with other nested groups
            assertTrue(parentContainer.isRunning());
        }
    }
}

Parallel Container Startup

Start multiple containers concurrently to reduce test setup time.

import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.postgresql.PostgreSQLContainer;
import org.testcontainers.containers.GenericContainer;
import org.junit.jupiter.api.Test;

@Testcontainers(parallel = true)
class ParallelStartupTest {

    @Container
    private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    @Container
    private static GenericContainer<?> redis = new GenericContainer<>("redis:7.0");

    @Container
    private static GenericContainer<?> mongo = new GenericContainer<>("mongo:7");

    @Test
    void test() {
        // All three containers started in parallel
        // Total startup time ≈ max(postgres, redis, mongo) instead of sum
        assertTrue(postgres.isRunning());
        assertTrue(redis.isRunning());
        assertTrue(mongo.isRunning());
    }
}

Performance Comparison:

ConfigurationStartup Time (example)
Sequential (parallel = false)45s (15s + 10s + 20s)
Parallel (parallel = true)20s (max of 15s, 10s, 20s)

Meta-Annotation Pattern

Create custom annotations that include @Container.

import org.testcontainers.junit.jupiter.Container;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

// Define custom meta-annotation
@Container
@Retention(RetentionPolicy.RUNTIME)
@interface DatabaseContainer {
}

// Use custom annotation
@Testcontainers
class MetaAnnotationTest {

    @DatabaseContainer  // Uses @Container internally
    private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    @Test
    void test() {
        assertTrue(postgres.isRunning());
    }
}

Integration with Test Lifecycle

TestLifecycleAware Interface

Containers implementing TestLifecycleAware receive callbacks before and after tests.

import org.testcontainers.lifecycle.TestLifecycleAware;
import org.testcontainers.lifecycle.TestDescription;
import org.testcontainers.containers.GenericContainer;

class CustomContainer extends GenericContainer<CustomContainer> implements TestLifecycleAware {

    public CustomContainer(String imageName) {
        super(imageName);
    }

    @Override
    public void beforeTest(TestDescription description) {
        // Called before each test
        System.out.println("Starting test: " + description.getTestId());
        // Can create test-specific resources
    }

    @Override
    public void afterTest(TestDescription description, Optional<Throwable> throwable) {
        // Called after each test
        if (throwable.isPresent()) {
            System.err.println("Test failed: " + throwable.get().getMessage());
            // Can capture logs, screenshots, etc.
        }
    }
}

@Testcontainers
class TestLifecycleTest {

    @Container
    private static CustomContainer container = new CustomContainer("myapp:latest");

    @Test
    void test() {
        // beforeTest() called automatically before this test
        // afterTest() called automatically after this test
    }
}

JUnit 5 Extension Points

The Testcontainers extension integrates with JUnit 5 lifecycle:

Test Class Lifecycle:
┌────────────────────────────────────────┐
│ @BeforeAll (static setup)              │
├────────────────────────────────────────┤
│ TestcontainersExtension.beforeAll()    │
│   → Start static @Container fields     │
│   → Call TestLifecycleAware.beforeTest │
├────────────────────────────────────────┤
│ @BeforeEach (instance setup)           │
├────────────────────────────────────────┤
│ TestcontainersExtension.beforeEach()   │
│   → Start instance @Container fields   │
│   → Call TestLifecycleAware.beforeTest │
├────────────────────────────────────────┤
│ @Test (test method)                    │
├────────────────────────────────────────┤
│ TestcontainersExtension.afterEach()    │
│   → Call TestLifecycleAware.afterTest  │
│   → Stop instance containers           │
├────────────────────────────────────────┤
│ @AfterEach (instance cleanup)          │
├────────────────────────────────────────┤
│ ... (repeat for each test) ...        │
├────────────────────────────────────────┤
│ TestcontainersExtension.afterAll()     │
│   → Call TestLifecycleAware.afterTest  │
│   → Stop static containers             │
├────────────────────────────────────────┤
│ @AfterAll (static cleanup)             │
└────────────────────────────────────────┘

Comparison: @Testcontainers vs Manual Management

With @Testcontainers (Recommended)

@Testcontainers
class AutomaticTest {
    @Container
    private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    @Test
    void test() {
        // Container automatically started and stopped
        assertNotNull(postgres.getJdbcUrl());
    }
}

Advantages:

  • ✅ No boilerplate code
  • ✅ Automatic lifecycle management
  • ✅ Works with test inheritance
  • ✅ Supports parallel startup
  • ✅ Integrates with TestLifecycleAware

Manual Management (Without @Testcontainers)

class ManualTest {
    private static PostgreSQLContainer<?> postgres;

    @BeforeAll
    static void setup() {
        postgres = new PostgreSQLContainer<>("postgres:15");
        postgres.start();
    }

    @AfterAll
    static void teardown() {
        if (postgres != null) {
            postgres.stop();
        }
    }

    @Test
    void test() {
        assertNotNull(postgres.getJdbcUrl());
    }
}

Disadvantages:

  • ❌ More boilerplate
  • ❌ Manual lifecycle management
  • ❌ Need to handle cleanup in finally blocks
  • ❌ More error-prone

Best Practices

1. Prefer Static Containers for Read-Only Tests

@Testcontainers
class ReadOnlyTests {
    // Shared - faster execution
    @Container
    private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    @Test
    void readTest1() { /* read-only */ }

    @Test
    void readTest2() { /* read-only */ }
}

2. Use Instance Containers for Stateful Tests

@Testcontainers
class StatefulTests {
    // Per-test - ensures isolation
    @Container
    private GenericContainer<?> redis = new GenericContainer<>("redis:7.0");

    @Test
    void modifyState1() { /* writes data */ }

    @Test
    void modifyState2() { /* writes data */ }
}

3. Enable Parallel Startup for Multiple Containers

@Testcontainers(parallel = true)  // Faster startup
class MultiContainerTests {
    @Container
    private static PostgreSQLContainer<?> postgres = ...;

    @Container
    private static GenericContainer<?> redis = ...;

    @Container
    private static GenericContainer<?> kafka = ...;
}

4. Use @EnabledIfDockerAvailable for Optional Docker Tests

@EnabledIfDockerAvailable  // Gracefully skip if no Docker
class DockerOptionalTests {
    @Test
    void dockerTest() { /* ... */ }
}

5. Organize Containers in Base Test Classes

@Testcontainers
abstract class DatabaseTestBase {
    @Container
    protected static PostgreSQLContainer<?> postgres = ...;

    protected String getJdbcUrl() {
        return postgres.getJdbcUrl();
    }
}

class UserRepositoryTest extends DatabaseTestBase {
    @Test
    void testUsers() {
        // Uses inherited postgres container
    }
}

Troubleshooting

Issue: Containers Not Starting

Problem: @Container fields not starting automatically

Solution: Ensure @Testcontainers annotation is present on the test class

// ❌ Wrong - missing @Testcontainers
class BrokenTest {
    @Container
    private static GenericContainer<?> container = ...;
}

// ✅ Correct
@Testcontainers
class WorkingTest {
    @Container
    private static GenericContainer<?> container = ...;
}

Issue: Container State Persists Between Tests

Problem: Tests interfere with each other due to shared container state

Solution: Use instance fields instead of static fields

// ❌ Shared state causes test pollution
@Testcontainers
class ProblematicTest {
    @Container
    private static GenericContainer<?> redis = ...;  // Shared

    @Test
    void test1() { /* modifies Redis */ }

    @Test
    void test2() { /* sees test1 modifications */ }
}

// ✅ Isolated per-test instances
@Testcontainers
class IsolatedTest {
    @Container
    private GenericContainer<?> redis = ...;  // Per-test

    @Test
    void test1() { /* modifies Redis */ }

    @Test
    void test2() { /* fresh Redis */ }
}

Issue: Parallel Execution Errors

Problem: Tests fail with parallel JUnit execution

Solution: The Testcontainers JUnit Jupiter extension is not designed for parallel test execution. Use sequential execution:

// junit-platform.properties
junit.jupiter.execution.parallel.enabled = false

Or use parallel = true parameter for parallel container startup (not test execution):

@Testcontainers(parallel = true)  // Parallel CONTAINER startup only
class SequentialTests {
    // Tests still run sequentially
}

Issue: Docker Not Available Errors

Problem: Tests fail when Docker is not running

Solutions:

  1. Option 1: Disable tests without Docker
@Testcontainers(disabledWithoutDocker = true)
class OptionalDockerTests { /* ... */ }
  1. Option 2: Use @EnabledIfDockerAvailable
@EnabledIfDockerAvailable
class ConditionalTests { /* ... */ }
  1. Option 3: Skip in CI without Docker
@BeforeAll
static void checkDocker() {
    assumeTrue(DockerClientFactory.instance().isDockerAvailable());
}

Issue: Containers Not Cleaned Up

Problem: Containers remain running after tests

Solution: Ensure proper test completion (no hanging assertions). JUnit's ExtensionContext automatically cleans up when context closes. If containers persist:

// Check for test framework issues
@Testcontainers
class CleanupTest {
    @Container
    private static GenericContainer<?> container = ...;

    @Test
    void test() {
        // Ensure test completes normally
        // Avoid: System.exit(), infinite loops, etc.
    }
}

Complete Example

import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.postgresql.PostgreSQLContainer;
import org.testcontainers.containers.GenericContainer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;

@Testcontainers(parallel = true)  // Start containers in parallel
class CompleteJUnitJupiterExample {

    // Shared database for all tests
    @Container
    private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    // Per-test cache
    @Container
    private GenericContainer<?> redis = new GenericContainer<>("redis:7.0")
        .withExposedPorts(6379);

    @BeforeAll
    static void setupDatabase() throws Exception {
        // Container already running - can execute setup
        try (Connection conn = DriverManager.getConnection(
                postgres.getJdbcUrl(),
                postgres.getUsername(),
                postgres.getPassword())) {

            conn.createStatement().execute(
                "CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(255))"
            );
        }
    }

    @Test
    void testDatabaseConnection() throws Exception {
        try (Connection conn = DriverManager.getConnection(
                postgres.getJdbcUrl(),
                postgres.getUsername(),
                postgres.getPassword())) {

            ResultSet rs = conn.createStatement().executeQuery("SELECT 1");
            assertTrue(rs.next());
            assertEquals(1, rs.getInt(1));
        }
    }

    @Test
    void testRedisConnection() {
        // Each test gets a fresh Redis instance
        assertTrue(redis.isRunning());
        int redisPort = redis.getMappedPort(6379);
        assertTrue(redisPort > 0);
    }

    @Nested
    class UserRepositoryTests {

        @Test
        void testInsertUser() throws Exception {
            // Can access parent containers
            try (Connection conn = DriverManager.getConnection(
                    postgres.getJdbcUrl(),
                    postgres.getUsername(),
                    postgres.getPassword())) {

                conn.createStatement().execute(
                    "INSERT INTO users (name) VALUES ('Alice')"
                );

                ResultSet rs = conn.createStatement().executeQuery(
                    "SELECT COUNT(*) FROM users"
                );
                assertTrue(rs.next());
                assertTrue(rs.getInt(1) > 0);
            }
        }
    }
}

This comprehensive JUnit Jupiter integration enables seamless container lifecycle management, reducing boilerplate and ensuring reliable test execution with automatic cleanup.