Residential Proxies Web Scraping API Turn Sites Into AI Data Automate 3000+ Apps Learn Python Automation Pay As You Go Proxies
Residential Proxies Web Scraping API
Pay As You Go Proxies 10 Free Proxies Antidetect Browser No Code Browser Bots Web Data For AI Agents Hire Scraper Builders

Test Containers: Isolated Environments for Every Test Run

Updated September 2026
Testcontainers is a library that starts Docker containers programmatically from within your test code, giving every test run fresh instances of databases, message brokers, caches, and other infrastructure services. Instead of maintaining shared test databases with stale data and version drift, each test gets an isolated environment that matches production exactly. This guide covers setup across languages, practical patterns for common databases and services, and configuration for CI pipelines.

The biggest source of unreliable integration tests is shared state. When multiple developers run tests against the same test database, or when CI pipelines reuse a database that was not properly cleaned up from the last run, tests fail for reasons unrelated to the code being tested. Someone else's test data causes unexpected query results. A schema migration was applied to one environment but not another. The test database is running PostgreSQL 14 while production uses PostgreSQL 16. Testcontainers eliminates all of these problems by creating a disposable environment per test run.

How Testcontainers Works

The lifecycle is simple. Before tests run, Testcontainers starts one or more Docker containers using images you specify (postgres:16, redis:7, mongo:7, etc.). Each container gets a random available port on the host machine, so multiple test runs can execute simultaneously without port conflicts. Your test code retrieves the container's host and port, uses them to configure the application's database connection or service client, and runs tests against the real service. When the test suite finishes (or the JVM/process exits), the containers are automatically stopped and removed.

The startup time for most containers is 2 to 5 seconds. PostgreSQL starts in about 3 seconds, Redis in under 1 second, Elasticsearch in about 8 seconds. This overhead is negligible compared to the time spent on the tests themselves, and it is a one-time cost per test run rather than per individual test. The containers persist for the duration of the test suite, so hundreds of tests share the same container without repeated startup costs.

Testcontainers requires Docker to be available. On developer machines, Docker Desktop or OrbStack handles this. In CI environments, most platforms provide Docker by default (GitHub Actions, GitLab CI, CircleCI all run jobs in environments with Docker available). For CI platforms that run jobs inside containers (Docker-in-Docker scenarios), Testcontainers supports alternative container runtimes and can be configured to use the host's Docker daemon.

Setup by Language

Node.js (JavaScript/TypeScript)

Install the testcontainers package:

npm install --save-dev testcontainers

Start a PostgreSQL container in your test setup:

import { PostgreSqlContainer } from '@testcontainers/postgresql';

let container;
let connectionUrl;

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

  connectionUrl = container.getConnectionUri();
  // Pass connectionUrl to your ORM or database client
}, 30000);

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

test('inserts and retrieves a user', async () => {
  // Your test code using the real PostgreSQL instance
});

The getConnectionUri() method returns a full connection string like postgresql://test:test@localhost:55432/testdb with the dynamically assigned port. Pass this to your application's database configuration and your tests run against a real PostgreSQL 16 instance that is completely isolated from everything else.

Python

Install the testcontainers package:

pip install testcontainers[postgres]

Use it in a pytest fixture:

import pytest
from testcontainers.postgres import PostgresContainer

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

@pytest.fixture
def db_connection(postgres):
    engine = create_engine(postgres.get_connection_url())
    # Run migrations
    Base.metadata.create_all(engine)
    with engine.connect() as conn:
        yield conn

def test_insert_user(db_connection):
    # Test against real PostgreSQL
    pass

The scope="session" on the fixture means the container starts once for the entire test session and is shared across all tests. If you need complete isolation per test (each test gets its own database), change the scope to "function" and accept the additional startup time.

Java

Add the Testcontainers dependency to your build file:

// Gradle
testImplementation 'org.testcontainers:testcontainers:1.20.0'
testImplementation 'org.testcontainers:postgresql:1.20.0'
testImplementation 'org.testcontainers:junit-jupiter:1.20.0'

Use the @Testcontainers annotation with JUnit 5:

@Testcontainers
class UserRepositoryTest {

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

    @BeforeAll
    static void setUp() {
        // Configure datasource with:
        // postgres.getJdbcUrl()
        // postgres.getUsername()
        // postgres.getPassword()
    }

    @Test
    void shouldInsertUser() {
        // Test against real PostgreSQL
    }
}

Java's Testcontainers has the most mature ecosystem with dedicated modules for over 50 services including PostgreSQL, MySQL, MariaDB, MongoDB, Redis, Elasticsearch, Kafka, RabbitMQ, Localstack (AWS services emulation), and many more.

Go

Install the testcontainers-go module:

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

Use it in a test function:

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

    pgContainer, err := postgres.Run(ctx,
        "postgres:16",
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
        testcontainers.WithWaitStrategy(
            wait.ForListeningPort("5432/tcp"),
        ),
    )
    defer pgContainer.Terminate(ctx)

    connStr, _ := pgContainer.ConnectionString(ctx)
    // Use connStr to connect and run tests
}

Common Service Containers

Redis

Redis containers start in under 1 second, making them nearly free to include in test suites. Use them to test caching logic, session storage, and pub/sub functionality against the real Redis engine rather than mocks:

// Node.js
import { GenericContainer } from 'testcontainers';

const redis = await new GenericContainer('redis:7')
  .withExposedPorts(6379)
  .start();

const redisHost = redis.getHost();
const redisPort = redis.getMappedPort(6379);

MongoDB

MongoDB containers test document operations, aggregation pipelines, and index behavior against the real MongoDB engine:

# Python
from testcontainers.mongodb import MongoDbContainer

with MongoDbContainer("mongo:7") as mongo:
    client = MongoClient(mongo.get_connection_url())
    db = client.testdb
    # Run tests against real MongoDB

Kafka

Kafka containers test event-driven architectures end to end: producer, broker, consumer. Testcontainers handles the ZooKeeper dependency (or uses KRaft mode for Kafka 3.3+) automatically:

// Java
@Container
static KafkaContainer kafka = new KafkaContainer(
    DockerImageName.parse("confluentinc/cp-kafka:7.6.0")
);

// Use kafka.getBootstrapServers() to configure producers/consumers

LocalStack (AWS Services)

LocalStack emulates AWS services (S3, DynamoDB, SQS, SNS, Lambda, and more) in a single container. Test your AWS integration code without connecting to real AWS, avoiding costs and credential management in test environments:

// Node.js
import { LocalstackContainer } from '@testcontainers/localstack';

const localstack = await new LocalstackContainer('localstack/localstack:3')
  .start();

const s3Client = new S3Client({
  endpoint: localstack.getConnectionUri(),
  region: 'us-east-1',
  credentials: { accessKeyId: 'test', secretAccessKey: 'test' },
});

Patterns for Effective Use

Session Scoped Containers

Start containers once per test session rather than per test. A PostgreSQL container takes 3 seconds to start. Starting it once for 200 tests adds 3 seconds total. Starting it per test adds 600 seconds (10 minutes). Use session/module-level fixtures or static containers with JUnit's @Container annotation.

Database Per Test with Schemas

Instead of starting a new container per test, create a new database or schema within the same container for each test. PostgreSQL supports creating databases in milliseconds. Each test gets complete isolation without container startup overhead:

// Create a fresh schema per test
const schemaName = `test_${randomUUID().replace(/-/g, '')}`;
await client.query(`CREATE SCHEMA ${schemaName}`);
await client.query(`SET search_path TO ${schemaName}`);

Transaction Rollback

Wrap each test in a database transaction that gets rolled back at the end. The test sees its own writes during execution, but nothing persists after the test finishes. This is the fastest isolation technique because it avoids both container startup and schema creation overhead.

Custom Docker Images

When your tests need specific database extensions, seed data, or configuration, build a custom Docker image and use it with Testcontainers. The image can include pre-loaded schemas, custom PostgreSQL extensions (PostGIS, pgvector), or specific configuration files that mirror production.

CI Configuration

Most CI platforms provide Docker by default, making Testcontainers work without special configuration. A few considerations:

GitHub Actions: Docker is available on ubuntu-latest runners out of the box. No additional setup needed. Set the environment variable TESTCONTAINERS_RYUK_DISABLED=true if you encounter issues with the resource reaper (Ryuk) container, though this is rarely needed on modern runners.

GitLab CI: When running jobs in Docker containers (the default), enable Docker-in-Docker by adding the docker:dind service and setting DOCKER_HOST=tcp://docker:2375. Alternatively, use the shell executor or a Kubernetes executor with Docker socket mounting.

Jenkins: Docker must be available on the agent where tests run. When using the Docker pipeline plugin, mount the host's Docker socket into the build container: -v /var/run/docker.sock:/var/run/docker.sock. This lets Testcontainers create sibling containers on the host rather than nested containers.

Cache Docker images in CI to avoid pulling them from the registry on every run. GitHub Actions can cache the Docker layer cache, and all platforms support pulling images in a setup step that runs before the test job.

Testcontainers vs Docker Compose vs Service Containers

Testcontainers provides the finest-grained control. Containers are started and stopped from within test code, so different test classes can use different database versions or configurations. The test code is self-documenting: you can see exactly which services a test needs by looking at its setup. Best for integration tests that need precise control over the environment.

Docker Compose defines the complete environment in a YAML file. The CI pipeline starts all services with one command, runs all tests, and tears everything down. Simpler to set up than Testcontainers but less flexible: all tests share the same containers, and you cannot vary the environment per test class. Best for smaller projects or when all tests need the same set of services.

CI service containers (GitHub Actions services, GitLab services) are similar to Docker Compose but managed by the CI platform. They are the simplest option, requiring just a few lines in the CI configuration file. Limited to services that can be started before the job begins and shared across all steps. Best for straightforward setups with one or two dependencies.

Key Takeaway

Testcontainers eliminates "works on my machine" problems by giving every test run a fresh, isolated environment with real databases and services. Start containers at the session level to keep overhead low, use transaction rollback or schema isolation for per-test freshness, and let Docker images match your production versions exactly.