Docs / Containers & Docker / Testcontainers for Integration Testing

Testcontainers for Integration Testing

By Admin · Mar 15, 2026 · Updated Apr 23, 2026 · 293 views · 3 min read

Testcontainers is a library that provides lightweight, disposable Docker containers for integration testing. Instead of mocking databases, message queues, and other services, you test against real instances that are automatically created and destroyed for each test run. This guide covers Testcontainers in Java, Node.js, Go, and Python.

Why Testcontainers?

  • Real dependencies — test against actual PostgreSQL, Redis, Kafka, not mocks
  • Isolation — each test run gets fresh containers
  • Reproducibility — same containers in CI and local development
  • No cleanup — containers are automatically removed after tests

Node.js (JavaScript/TypeScript)

// Install
// npm install @testcontainers/postgresql testcontainers

import { PostgreSqlContainer } from "@testcontainers/postgresql";
import { Client } from "pg";

describe("User Repository", () => {
    let container;
    let client;

    beforeAll(async () => {
        container = await new PostgreSqlContainer("postgres:16")
            .withDatabase("testdb")
            .withUsername("test")
            .withPassword("test")
            .start();

        client = new Client({
            connectionString: container.getConnectionUri(),
        });
        await client.connect();

        // Run migrations
        await client.query(`
            CREATE TABLE users (
                id SERIAL PRIMARY KEY,
                email VARCHAR(255) UNIQUE NOT NULL,
                name VARCHAR(255) NOT NULL
            )
        `);
    }, 60000);

    afterAll(async () => {
        await client.end();
        await container.stop();
    });

    test("should insert and retrieve a user", async () => {
        await client.query(
            "INSERT INTO users (email, name) VALUES ($1, $2)",
            ["test@example.com", "Test User"]
        );
        const result = await client.query("SELECT * FROM users WHERE email = $1", ["test@example.com"]);
        expect(result.rows[0].name).toBe("Test User");
    });
});

Python

# pip install testcontainers[postgres]

from testcontainers.postgres import PostgresContainer
import psycopg2
import pytest

@pytest.fixture(scope="module")
def postgres():
    with PostgresContainer("postgres:16") as pg:
        yield pg

@pytest.fixture
def db_connection(postgres):
    conn = psycopg2.connect(
        host=postgres.get_container_host_ip(),
        port=postgres.get_exposed_port(5432),
        user=postgres.username,
        password=postgres.password,
        dbname=postgres.dbname,
    )
    yield conn
    conn.close()

def test_insert_user(db_connection):
    cur = db_connection.cursor()
    cur.execute("CREATE TABLE IF NOT EXISTS users (id SERIAL, name TEXT)")
    cur.execute("INSERT INTO users (name) VALUES (%s) RETURNING id", ("Alice",))
    user_id = cur.fetchone()[0]
    db_connection.commit()
    assert user_id == 1

Go

// go get github.com/testcontainers/testcontainers-go
// go get github.com/testcontainers/testcontainers-go/modules/postgres

package repository_test

import (
    "context"
    "testing"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
    "github.com/jackc/pgx/v5"
)

func TestUserRepository(t *testing.T) {
    ctx := context.Background()

    pgContainer, err := postgres.Run(ctx,
        "postgres:16",
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
    )
    if err != nil {
        t.Fatal(err)
    }
    defer pgContainer.Terminate(ctx)

    connStr, _ := pgContainer.ConnectionString(ctx, "sslmode=disable")
    conn, err := pgx.Connect(ctx, connStr)
    if err != nil {
        t.Fatal(err)
    }
    defer conn.Close(ctx)

    // Test actual database operations
    _, err = conn.Exec(ctx, "CREATE TABLE users (id SERIAL, name TEXT)")
    if err != nil {
        t.Fatal(err)
    }
}

Available Modules

# Databases: PostgreSQL, MySQL, MongoDB, Redis, Elasticsearch, CockroachDB
# Message Queues: Kafka, RabbitMQ, NATS, Pulsar
# Cloud: LocalStack (AWS), MinIO (S3), Azurite (Azure)
# Other: Nginx, Vault, Keycloak, Selenium, K3s

# Generic container for anything with a Docker image
import { GenericContainer } from "testcontainers";

const container = await new GenericContainer("redis:7")
    .withExposedPorts(6379)
    .withStartupTimeout(30000)
    .start();

CI/CD Integration

# GitHub Actions — Docker is available by default
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run integration tests
        run: npm test
        # Testcontainers automatically uses Docker in CI

Best Practices

  • Use module-specific containers (e.g., PostgreSqlContainer) instead of GenericContainer for better defaults
  • Set scope: "module" on fixtures to reuse containers across tests in the same file
  • Use withReuse() for development to avoid recreating containers on every test run
  • Set startup timeouts appropriate for your CI environment (containers take longer in CI)
  • Run migrations in the test setup, not in the container initialization
  • Use Testcontainers for integration tests, not unit tests — keep unit tests fast and mock-based

Was this article helpful?