Introduction

Welcome back, intrepid developer! In our previous chapters, we learned why Testcontainers is a game-changer for robust, reliable integration and end-to-end testing. We understood how it leverages Docker to provide disposable, real-world dependencies without the headaches of managing complex test environments or falling into the trap of unreliable mocks.

Now, it’s time to roll up our sleeves and explore the how. This chapter dives deep into the heart of Testcontainers: its Core API. We’ll uncover two powerful ways to interact with Docker containers for your tests: using GenericContainer for ultimate flexibility with any Docker image, and leveraging specialized “Modules” that offer convenient, idiomatic APIs for common services like databases and message brokers. By the end, you’ll be confidently spinning up and managing containerized services across Java, JavaScript, and Python.

To get the most out of this chapter, ensure you have:

  • Docker Desktop (or Docker Engine) installed and running.
  • A basic understanding of Docker commands (like docker run, docker ps).
  • Familiarity with your chosen programming language(s) (Java, JavaScript/TypeScript, Python) and their respective testing frameworks.

Let’s make our tests truly integrated!

Core Concepts: The Two Flavors of Containers

Testcontainers offers two primary mechanisms for working with containers in your tests, each with its own strengths. Think of them as your general-purpose screwdriver and your specialized power drill.

GenericContainer: Your Docker Swiss Army Knife

The GenericContainer is the most fundamental and flexible component of Testcontainers. It allows you to run any Docker image available on Docker Hub (or a private registry) directly from your test code.

What it is: GenericContainer is a class (or equivalent construct in different languages) that provides a programmatic interface to launch, configure, and manage a Docker container. You specify the Docker image name, and Testcontainers handles the rest.

Why it’s important:

  • Ultimate Flexibility: If there isn’t a specific Testcontainers module for a service you need, GenericContainer is your go-to. This is perfect for custom microservices, niche databases, or any bespoke Docker image you might have.
  • Foundation: Under the hood, many of the specialized modules we’ll discuss next actually extend or wrap GenericContainer. Understanding it gives you a solid foundation for how Testcontainers operates.

How it works: You’ll typically provide:

  1. Image Name: The name of the Docker image (e.g., "nginx:latest").
  2. Exposed Ports: Which ports inside the container you want to be accessible from your test (Testcontainers dynamically maps these to available host ports).
  3. Wait Strategy: How Testcontainers should determine when the container is “ready” to accept connections or serve requests. This is crucial for reliable tests! Common strategies include waiting for a specific log message, a successful HTTP health check, or a specified port to be open.
  4. Environment Variables: Any configuration passed to the container at startup.

Specialized Modules: Convenience and Best Practices

While GenericContainer is powerful, configuring common services like PostgreSQL, Kafka, or Redis from scratch can become repetitive. This is where Testcontainers’ specialized modules shine!

What they are: Testcontainers provides a rich ecosystem of pre-built modules for popular services. These modules encapsulate the best practices for setting up and interacting with these services. For example, there’s a PostgreSQLContainer module, a KafkaContainer module, and many more.

Why they’re great:

  • Convenience: They abstract away common configurations. Instead of manually setting environment variables for a database’s username and password, the module provides dedicated methods like withUsername() and withPassword().
  • Sensible Defaults: Modules often come with pre-configured wait strategies, default ports, and other settings that just “work” out of the box.
  • Service-Specific APIs: They offer helper methods tailored to the service. For instance, PostgreSQLContainer can directly give you a JDBC connection URL (Java) or connection string (JS/Python) that correctly uses the dynamically mapped port.
  • Reduced Boilerplate: Less code for you to write, meaning faster test development.

How they work: These modules typically extend GenericContainer but add:

  • Pre-defined image names and versions.
  • Default exposed ports.
  • Intelligent wait strategies specific to the service.
  • Convenience methods for common configuration options.
  • Helper methods to retrieve connection details (e.g., connection strings).

In essence, modules make your life easier by doing much of the GenericContainer setup for you, allowing you to focus on writing your actual test logic.

Deep Dive into Generic Containers

Let’s get our hands dirty by spinning up a basic Nginx web server using GenericContainer across Java, JavaScript/TypeScript, and Python. Nginx is a great choice because it’s simple, widely used, and illustrates basic container interaction.

Java Example: Running Nginx with GenericContainer

First, ensure you have a Java project set up (e.g., a Maven or Gradle project). We’ll use JUnit 5 for our tests.

1. Dependencies (Maven pom.xml): We’ll need the core Testcontainers library. As of 2026-02-14, the latest stable Testcontainers for Java is around 1.19.4.

<!-- pom.xml -->
<project>
    <dependencies>
        <!-- Testcontainers Core -->
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers</artifactId>
            <version>1.19.4</version>
            <scope>test</scope>
        </dependency>
        <!-- JUnit Jupiter API for writing tests -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.10.1</version>
            <scope>test</scope>
        </dependency>
        <!-- JUnit Jupiter Engine for running tests -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.10.1</version>
            <scope>test</scope>
        </dependency>
        <!-- Apache HttpClient for making HTTP requests -->
        <dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5</artifactId>
            <version>5.2.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!-- ... other project configurations ... -->
</project>
  • testcontainers: This is the core library that provides GenericContainer.
  • junit-jupiter-api / junit-jupiter-engine: Standard JUnit 5 dependencies.
  • httpclient5: A simple library to make HTTP requests to our Nginx container.

2. Create Your Test Class (NginxGenericContainerTest.java):

// src/test/java/com/example/NginxGenericContainerTest.java
package com.example;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class NginxGenericContainerTest {

    // 1. Declare our GenericContainer. We use 'static' for 'BeforeAll/AfterAll' lifecycle.
    // DockerImageName.parse("nginx:latest") is a best practice for image names.
    public static GenericContainer<?> nginx = new GenericContainer<>(DockerImageName.parse("nginx:latest"))
            .withExposedPorts(80) // 2. Expose port 80 (Nginx default HTTP port)
            // 3. Define a wait strategy: wait for the HTTP port 80 to be available
            .waitingFor(Wait.forHttp("/").forPort(80));

    @BeforeAll
    static void startContainer() {
        // 4. Start the container before all tests run
        nginx.start();
        System.out.println("Nginx container started on port: " + nginx.getMappedPort(80));
    }

    @AfterAll
    static void stopContainer() {
        // 5. Stop the container after all tests complete
        nginx.stop();
        System.out.println("Nginx container stopped.");
    }

    @Test
    void nginxIsRunningAndServesDefaultPage() throws IOException, InterruptedException {
        // 6. Get the dynamically mapped port from the host machine
        Integer mappedPort = nginx.getMappedPort(80);
        String host = nginx.getHost(); // Get the host address (usually "localhost")

        // 7. Construct the URL to access Nginx
        String url = String.format("http://%s:%d", host, mappedPort);
        System.out.println("Attempting to connect to Nginx at: " + url);

        // 8. Make an HTTP GET request to Nginx
        HttpClient client = HttpClient.newBuilder().build();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .GET()
                .build();

        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        // 9. Assertions to verify Nginx is working
        assertEquals(200, response.statusCode(), "Expected HTTP 200 OK from Nginx");
        assertTrue(response.body().contains("Welcome to nginx!"), "Expected Nginx default welcome page");
        System.out.println("Received Nginx response: " + response.body().substring(0, Math.min(100, response.body().length())) + "...");
    }
}
  • nginx declaration: We create an instance of GenericContainer pointing to the nginx:latest image. DockerImageName.parse() is the recommended way to specify images.
  • .withExposedPorts(80): This tells Testcontainers that port 80 inside the container should be exposed to the host machine. Testcontainers will automatically find a free port on your host and map it.
  • .waitingFor(Wait.forHttp("/").forPort(80)): This is our “wait strategy.” We’re telling Testcontainers to consider Nginx “ready” when it responds with an HTTP 200 status code to a GET request on the root path (/) of its exposed port 80. This prevents our test from trying to connect before Nginx has fully started.
  • @BeforeAll / @AfterAll: These JUnit 5 annotations ensure the container starts once before all tests in the class and stops once after all tests complete.
  • nginx.getMappedPort(80): This crucial method retrieves the actual port on your host machine that Testcontainers has mapped to the container’s internal port 80. Always use this to connect to your containerized service.
  • HTTP Client: We use Java’s built-in HttpClient to make a request to Nginx and verify its response.

Run this test, and you’ll see a real Nginx server spin up, serve a page, and then gracefully shut down. Pretty neat, right?

JavaScript/TypeScript Example: Running Nginx with GenericContainer

For Node.js, we’ll use the testcontainers library. Ensure you have Node.js and npm/yarn installed.

1. Project Setup:

# Create a new project directory
mkdir testcontainers-nginx-js && cd testcontainers-nginx-js
# Initialize npm project
npm init -y
# Install dependencies
npm install --save-dev [email protected] @types/jest jest axios @types/axios
# For TypeScript:
npm install --save-dev [email protected] [email protected] @types/[email protected]
  • testcontainers: The core Testcontainers library for Node.js (latest stable around 10.x.x as of 2026-02-14).
  • jest: A popular JavaScript testing framework.
  • axios: A promise-based HTTP client for making requests.
  • typescript, ts-node, @types/node: For TypeScript development.

2. Configure tsconfig.json (for TypeScript):

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node"
  },
  "include": ["src/**/*.ts", "test/**/*.ts"],
  "exclude": ["node_modules"]
}

3. Create Your Test File (nginx.test.ts):

// test/nginx.test.ts
import { GenericContainer, Wait } from 'testcontainers';
import axios from 'axios';

describe('Nginx GenericContainer Test', () => {
    let nginxContainer: GenericContainer;
    let containerHost: string;
    let mappedPort: number;

    // This runs once before all tests in this describe block
    beforeAll(async () => {
        // 1. Declare and configure GenericContainer
        nginxContainer = await new GenericContainer('nginx:latest')
            .withExposedPorts(80) // 2. Expose internal port 80
            // 3. Wait strategy: wait for HTTP 200 on '/'
            .waitingFor(Wait.forHttp('/').forPort(80))
            .start(); // 4. Start the container

        containerHost = nginxContainer.getHost(); // Get the host (e.g., 'localhost')
        mappedPort = nginxContainer.getMappedPort(80); // 5. Get the dynamically mapped host port

        console.log(`Nginx container started on ${containerHost}:${mappedPort}`);
    }, 60000); // Increase Jest timeout for container startup

    // This runs once after all tests in this describe block
    afterAll(async () => {
        if (nginxContainer) {
            await nginxContainer.stop(); // 6. Stop the container
            console.log('Nginx container stopped.');
        }
    });

    it('should have Nginx running and serve the default page', async () => {
        // 7. Construct the URL to access Nginx
        const url = `http://${containerHost}:${mappedPort}`;
        console.log(`Attempting to connect to Nginx at: ${url}`);

        // 8. Make an HTTP GET request
        const response = await axios.get(url);

        // 9. Assertions
        expect(response.status).toBe(200);
        expect(response.data).toContain('Welcome to nginx!');
        console.log(`Received Nginx response: ${response.data.substring(0, Math.min(100, response.data.length))}...`);
    });
});
  • GenericContainer: Similar to Java, we instantiate it with the image name.
  • .withExposedPorts(80): Exposes the internal Nginx port.
  • .waitingFor(...): Our wait strategy.
  • .start(): Asynchronously starts the container.
  • beforeAll/afterAll: Jest hooks for setting up and tearing down the container. async/await is essential here for managing asynchronous container operations.
  • nginxContainer.getHost() / nginxContainer.getMappedPort(80): Retrieve connection details.
  • axios: Used to make the HTTP request and verify Nginx’s response.

To run this test, add a test script to your package.json:

// package.json
{
  // ...
  "scripts": {
    "test": "jest --testMatch=\"<rootDir>/test/**/*.test.ts\""
  },
  // ...
}

Then execute:

npm test

Python Example: Running Nginx with GenericContainer

For Python, we’ll use the testcontainers-python library and pytest for testing.

1. Project Setup:

# Create a new project directory
mkdir testcontainers-nginx-py && cd testcontainers-nginx-py
# Create a virtual environment
python -m venv venv
source venv/bin/activate # On Windows: .\venv\Scripts\activate
# Install dependencies
pip install testcontainers-python==4.14.1 pytest==8.1.1 requests==2.31.0
  • testcontainers-python: The official Python binding for Testcontainers (latest stable around 4.14.1 as of 2026-02-14).
  • pytest: A popular Python testing framework.
  • requests: A simple HTTP library for making requests.

2. Create Your Test File (test_nginx.py):

# test_nginx.py
import pytest
import requests
from testcontainers.core.container import GenericContainer
from testcontainers.core.waiting_utils import wait_for_http_status_code

# 1. Declare our GenericContainer with pytest fixture for lifecycle management
@pytest.fixture(scope="module")
def nginx_container():
    # 2. Configure GenericContainer
    with GenericContainer("nginx:latest") \
            .with_exposed_ports(80) \
            .wait_for_http("/"): # 3. Wait strategy: wait for HTTP 200 on '/'
        # 4. The 'yield' keyword starts the container and makes it available to tests
        yield # Container starts here
        # Code after 'yield' runs when the container is stopped (after all tests)

# Our test function, which takes the fixture as an argument
def test_nginx_is_running_and_serves_default_page(nginx_container):
    # 5. Get the dynamically mapped port from the host
    mapped_port = nginx_container.get_mapped_port(80)
    host = nginx_container.get_container_host_ip() # Get host IP

    # 6. Construct the URL
    url = f"http://{host}:{mapped_port}"
    print(f"Attempting to connect to Nginx at: {url}")

    # (Optional) Explicitly wait for HTTP status code if not using wait_for_http in fixture,
    # but with wait_for_http in fixture, this is often redundant for readiness.
    # We still need to wait until the URL is actually accessible after container reports ready.
    wait_for_http_status_code(url, 200, timeout=10)

    # 7. Make an HTTP GET request
    response = requests.get(url)

    # 8. Assertions
    assert response.status_code == 200, "Expected HTTP 200 OK from Nginx"
    assert "Welcome to nginx!" in response.text, "Expected Nginx default welcome page"
    print(f"Received Nginx response: {response.text[:100]}...")
  • @pytest.fixture(scope="module"): Pytest fixtures are excellent for managing resource lifecycles. scope="module" means the nginx_container will be started once for all tests in the module and stopped afterwards.
  • with GenericContainer(...): The with statement ensures the container is properly started and stopped, similar to start()/stop() in other languages, but managed by the fixture.
  • .with_exposed_ports(80): Exposes the internal port.
  • .wait_for_http("/"): Python’s wait strategy for HTTP readiness.
  • yield: This makes the container instance available to any test function that requests the nginx_container fixture. The code after yield runs for cleanup.
  • nginx_container.get_mapped_port(80) / nginx_container.get_container_host_ip(): Retrieve connection details.
  • requests: Used to make the HTTP request and verify.

To run this test:

pytest

Specialized Modules in Action

Now, let’s explore how specialized modules simplify interaction with common services. We’ll focus on PostgreSQL, a widely used database.

PostgreSQL Container (Java)

1. Dependencies (Maven pom.xml): Add the testcontainers-postgresql module and a PostgreSQL JDBC driver.

<!-- pom.xml -->
<project>
    <dependencies>
        <!-- ... existing dependencies for JUnit and Testcontainers Core ... -->

        <!-- Testcontainers PostgreSQL Module -->
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>postgresql</artifactId>
            <version>1.19.4</version> <!-- Match Testcontainers Core version -->
            <scope>test</scope>
        </dependency>
        <!-- PostgreSQL JDBC Driver -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.7.1</version> <!-- Latest stable as of 2026-02-14 -->
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!-- ... -->
</project>

2. Create Your Test Class (PostgreSqlModuleTest.java):

// src/test/java/com/example/PostgreSqlModuleTest.java
package com.example;

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

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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@Testcontainers // Enables automatic lifecycle management for @Container fields
public class PostgreSqlModuleTest {

    // 1. Declare the PostgreSQLContainer. Testcontainers automatically starts/stops it.
    // Use the latest official image, e.g., "postgres:16.1"
    @Container
    public static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
            "postgres:16.1" // Specify the Docker image and version
        )
        .withDatabaseName("testdb")
        .withUsername("testuser")
        .withPassword("testpass");

    // No @BeforeAll/@AfterAll needed if using @Testcontainers and @Container for simple cases!
    // Testcontainers handles start/stop automatically.

    @Test
    void canConnectToPostgresAndExecuteQuery() throws SQLException {
        // 2. The module provides easy access to connection details
        String jdbcUrl = postgres.getJdbcUrl();
        String username = postgres.getUsername();
        String password = postgres.getPassword();

        System.out.println("Connecting to PostgreSQL at: " + jdbcUrl);

        try (Connection connection = DriverManager.getConnection(jdbcUrl, username, password);
             Statement statement = connection.createStatement()) {

            // 3. Verify connection and execute a simple query
            assertTrue(connection.isValid(5), "Connection to PostgreSQL should be valid");

            statement.execute("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(255))");
            statement.executeUpdate("INSERT INTO users (name) VALUES ('Alice'), ('Bob')");

            ResultSet rs = statement.executeQuery("SELECT COUNT(*) FROM users");
            assertTrue(rs.next());
            assertEquals(2, rs.getInt(1), "Expected 2 users in the table");

            ResultSet namesRs = statement.executeQuery("SELECT name FROM users ORDER BY name");
            assertTrue(namesRs.next());
            assertEquals("Alice", namesRs.getString("name"));
            assertTrue(namesRs.next());
            assertEquals("Bob", namesRs.getString("name"));

            System.out.println("PostgreSQL operations successful!");
        }
    }
}
  • @Testcontainers: This JUnit 5 extension handles the start() and stop() lifecycle of @Container fields automatically. This is a common and highly recommended pattern.
  • @Container public static PostgreSQLContainer<?> postgres: We declare our container field. Testcontainers picks up on the PostgreSQLContainer type and the specified Docker image.
  • .withDatabaseName(), .withUsername(), .withPassword(): These are convenient methods provided by the PostgreSQLContainer module, abstracting away generic environment variables.
  • postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword(): The module provides direct access to the correctly configured connection details, including the dynamically mapped port. No manual port mapping lookup needed!
  • JDBC connection: We use standard Java SQL APIs to connect and interact with the database, just as we would with any other PostgreSQL instance.

This example clearly shows how modules streamline the process. No withExposedPorts or waitingFor explicit calls are needed because the module handles those details intelligently.

PostgreSQL Container (JavaScript/TypeScript)

1. Project Setup:

# Assuming you're in the same testcontainers-nginx-js directory or a new one
npm install --save-dev [email protected] [email protected] @types/[email protected]
  • pg: The official PostgreSQL client for Node.js (latest stable around 8.x.x as of 2026-02-14).

2. Create Your Test File (postgresql.test.ts):

// test/postgresql.test.ts
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { Client } from 'pg'; // PostgreSQL client
import { type StartedTestContainer } from 'testcontainers'; // Import type for clarity

describe('PostgreSQL Module Test', () => {
    let postgresContainer: StartedTestContainer; // Use StartedTestContainer for type safety
    let pgClient: Client;

    beforeAll(async () => {
        // 1. Declare and start the PostgreSQL container using the module
        postgresContainer = await new PostgreSqlContainer('postgres:16.1') // Specify image version
            .withDatabase('testdb')
            .withUsername('testuser')
            .withPassword('testpass')
            .start(); // This also waits for readiness internally

        // 2. Get the connection string from the module
        const connectionUri = postgresContainer.getConnectionUri(); // Provides correct URI

        console.log(`PostgreSQL container started. Connecting with URI: ${connectionUri}`);

        // 3. Connect using the pg client
        pgClient = new Client({ connectionString: connectionUri });
        await pgClient.connect();

    }, 60000); // Increase Jest timeout

    afterAll(async () => {
        if (pgClient) {
            await pgClient.end(); // Close DB connection
            console.log('PostgreSQL client disconnected.');
        }
        if (postgresContainer) {
            await postgresContainer.stop(); // Stop the container
            console.log('PostgreSQL container stopped.');
        }
    });

    it('should connect to PostgreSQL and execute a query', async () => {
        // 4. Verify connection
        const { rows: [{ now }] } = await pgClient.query('SELECT NOW()');
        expect(now).toBeDefined();
        console.log(`Connected to PostgreSQL, server time: ${now}`);

        // 5. Create table and insert data
        await pgClient.query('CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(255))');
        await pgClient.query("INSERT INTO users (name) VALUES ('Alice'), ('Bob')");

        // 6. Query data
        const { rows: countRows } = await pgClient.query('SELECT COUNT(*) FROM users');
        expect(Number(countRows[0].count)).toBe(2);

        const { rows: nameRows } = await pgClient.query('SELECT name FROM users ORDER BY name');
        expect(nameRows.map(row => row.name)).toEqual(['Alice', 'Bob']);

        console.log('PostgreSQL operations successful!');
    });
});
  • @testcontainers/postgresql: This package provides the PostgreSqlContainer.
  • .withDatabase(), .withUsername(), .withPassword(): Module-specific configuration methods.
  • .getConnectionUri(): This method directly gives you a ready-to-use connection string for the Node.js pg client, including the dynamically mapped port.
  • pg.Client: We use the standard Node.js PostgreSQL client to interact with the database.

PostgreSQL Container (Python)

1. Project Setup:

# Assuming you're in the same testcontainers-nginx-py directory or a new one
pip install testcontainers-python[postgresql]==4.14.1 psycopg2-binary==2.9.9
  • testcontainers-python[postgresql]: This installs the core library along with the PostgreSQL module-specific dependencies.
  • psycopg2-binary: A popular PostgreSQL adapter for Python (latest stable around 2.9.9 as of 2026-02-14).

2. Create Your Test File (test_postgresql.py):

# test_postgresql.py
import pytest
import psycopg2

from testcontainers.postgres import PostgreSQLContainer

@pytest.fixture(scope="module")
def postgres_container():
    # 1. Declare and configure PostgreSQL container using the module
    with PostgreSQLContainer("postgres:16.1") \
            .with_database_name("testdb") \
            .with_user("testuser") \
            .with_password("testpass") as postgres:
        # The container starts here and waits for readiness internally
        yield postgres # Make the configured container available to tests

def test_can_connect_to_postgres_and_execute_query(postgres_container):
    # 2. Get connection details from the module
    conn_string = postgres_container.get_connection_url() # Provides connection URL

    print(f"Connecting to PostgreSQL at: {conn_string}")

    # 3. Connect using psycopg2
    with psycopg2.connect(conn_string) as conn:
        with conn.cursor() as cur:
            # 4. Verify connection and execute a simple query
            cur.execute("SELECT 1")
            assert cur.fetchone()[0] == 1, "Connection to PostgreSQL should be valid"

            cur.execute("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(255))")
            cur.execute("INSERT INTO users (name) VALUES ('Alice'), ('Bob')")
            conn.commit() # Commit changes for inserts

            cur.execute("SELECT COUNT(*) FROM users")
            assert cur.fetchone()[0] == 2, "Expected 2 users in the table"

            cur.execute("SELECT name FROM users ORDER BY name")
            names = [row[0] for row in cur.fetchall()]
            assert names == ['Alice', 'Bob']

            print("PostgreSQL operations successful!")
  • PostgreSQLContainer: The dedicated module class.
  • .with_database_name(), .with_user(), .with_password(): Pythonic configuration methods.
  • .get_connection_url(): Provides the complete connection URL, ready for psycopg2.
  • psycopg2.connect(): Standard Python PostgreSQL client usage.

Kafka Container (Concept Overview)

Testing microservices that communicate via message brokers like Apache Kafka is a classic integration testing challenge. Testcontainers makes it surprisingly straightforward with its KafkaContainer module.

Why it’s important:

  • Real Kafka, not Mocks: Ensures your producers and consumers interact with a real Kafka cluster, uncovering issues that mocks might miss (e.g., serialization/deserialization problems, Kafka version specific behaviors).
  • Full Stack Integration: Critical for testing event-driven architectures where services communicate asynchronously.

How it simplifies things: The KafkaContainer module (available in Java, JS/TS, Python) typically:

  • Spins up both a Kafka broker and its required ZooKeeper instance (or uses the newer KRaft mode for Kafka 2.8+).
  • Configures internal networking for Kafka to be accessible within the container network.
  • Provides helper methods to retrieve the bootstrap servers address, which your Kafka clients use to connect.
  • Comes with intelligent wait strategies to ensure Kafka is fully ready to accept client connections before your tests run.

While we won’t show full code examples to keep this chapter focused, remember that using the KafkaContainer module follows the same patterns as PostgreSQLContainer:

  1. Add the kafka module dependency.
  2. Instantiate KafkaContainer.
  3. Start it (using @Container, beforeAll, or pytest.fixture).
  4. Retrieve getBootstrapServers() (or equivalent) for your Kafka producer/consumer clients.
  5. Write your Kafka-interacting test logic!

Mini-Challenge: Redis with GenericContainer

You’ve seen both GenericContainer and specialized modules. Now it’s your turn to apply what you’ve learned.

Challenge: Spin up a Redis container using the GenericContainer class (or its language equivalent). Then, write a test that connects to it and verifies you can SET and GET a simple key-value pair.

Hint:

  • Redis uses port 6379 by default.
  • A good wait strategy for Redis might be Wait.forLogMessage("Ready to accept connections", 1).
  • You’ll need a Redis client library for your chosen language (e.g., Jedis for Java, ioredis for JS, redis-py for Python).

What to Observe/Learn:

  • How to correctly specify ports and wait strategies for a non-HTTP service.
  • Integrating a third-party client library with Testcontainers’ dynamically mapped ports.
  • The versatility of GenericContainer for services without a dedicated module (or when you want more fine-grained control).

Common Pitfalls & Troubleshooting

Even with Testcontainers, you might encounter issues. Here are some common ones and how to debug them:

  1. Container Not Starting or Timing Out:

    • Symptom: Your test hangs, then fails with a timeout error (ContainerLaunchException in Java, TimeoutError in JS/Python).
    • Cause:
      • Docker Daemon Not Running: The most common culprit. Ensure Docker Desktop/Engine is active.
      • Incorrect Image Name: Double-check the spelling of the Docker image (e.g., postgress instead of postgres).
      • Firewall Issues: Your firewall might be blocking Docker’s communication.
      • Resource Constraints: Docker might not have enough memory or CPU allocated, or your machine is simply overloaded.
      • Incorrect Wait Strategy: The container might be running, but your wait strategy isn’t detecting its readiness. For example, Wait.forHttp on a non-HTTP service, or waiting for a log message that never appears.
    • Fix:
      • Verify Docker is running (docker info).
      • Check the Docker image name.
      • Temporarily disable your firewall for testing.
      • Increase Docker’s allocated resources.
      • Review container logs (nginx.getLogs() in Java) to see if it’s printing expected “ready” messages or erroring out during startup. Adjust your wait strategy accordingly.
  2. Connection Refused / Cannot Connect to Service:

    • Symptom: Your client code (e.g., JDBC, Axios, Requests) throws “connection refused” or similar errors.
    • Cause:
      • Using Internal Container Port: You’re trying to connect to port 80 (internal Nginx port) instead of the dynamically mapped host port (e.g., 32768).
      • Wrong Host: Trying to connect to localhost when Testcontainers has mapped to a specific Docker host IP (less common with modern Docker setups but possible, especially in CI).
      • Service Not Ready: Your wait strategy might have passed, but the service isn’t fully initialized or able to handle connections yet.
    • Fix:
      • ALWAYS use getMappedPort() (or equivalent) to retrieve the host port.
      • ALWAYS use getHost() / getContainerHostIp() (or equivalent) for the host address.
      • Add small, cautious delays (e.g., Thread.sleep or await delay()) after container startup, or refine your wait strategy to be more robust.
      • Check Testcontainers’ log output for the mapped ports.
  3. Too Many Open Files / Resource Exhaustion:

    • Symptom: Tests fail intermittently with resource-related errors.
    • Cause: If you’re not properly stopping containers (e.g., forgetting stop() in afterAll hooks or not using with statements/fixtures), Docker resources can accumulate.
    • Fix: Ensure proper container lifecycle management. Use @AfterAll or afterAll/finally blocks, or pytest fixtures (scope="session" or scope="module" with yield) to guarantee containers are stopped.

Summary

Phew! You’ve just taken a huge leap into understanding the core of Testcontainers. Let’s recap what we’ve covered:

  • GenericContainer: Your fundamental tool for running any Docker image, providing unparalleled flexibility for custom services and unique testing scenarios. You learned to configure image names, exposed ports, and crucial wait strategies.
  • Specialized Modules: The convenience layer built on GenericContainer. These modules abstract away common boilerplate for popular services like PostgreSQL and Kafka, providing sensible defaults and idiomatic APIs.
  • Hands-on Examples: We successfully spun up Nginx with GenericContainer and PostgreSQL with its dedicated module across Java, JavaScript/TypeScript, and Python, demonstrating the language-specific implementations and patterns.
  • Mini-Challenge: You got a chance to apply your knowledge by setting up a Redis container.
  • Troubleshooting: We addressed common issues like container startup failures and connection problems, empowering you to debug your Testcontainers setups effectively.

You now have the foundational skills to leverage disposable containers for robust integration testing. In the next chapter, we’ll build on this by exploring advanced configuration options, including environment variables, volume mapping, network configuration, and how to manage multiple interdependent containers. Get ready to elevate your testing even further!

References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.