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