When you write integration tests that hit a real database, you face a trade-off: use an in-memory mock (fast but inaccurate) or connect to a real database (accurate but complex to manage). Testcontainers solves this by programmatically spinning up real Docker containers for each test run, then tearing them down automatically.

In this guide, we compare Testcontainers across four language ecosystems — Java, Go, Python, and Node.js — so you can pick the right library for your stack and set up reliable, repeatable database testing in your CI pipeline.

What Is Testcontainers?

Testcontainers is an open-source library that provides lightweight, throwaway instances of common databases, message brokers, web browsers, or anything else that can run in a Docker container. Instead of mocking your database layer or maintaining a shared test database, each test gets its own isolated container.

The library handles the full lifecycle: pulling the Docker image, starting the container, waiting for the service to be ready (via health checks or log pattern matching), exposing the connection details to your test code, and cleaning up when the test completes.

FeatureTestcontainers JavaTestcontainers GoTestcontainers PythonTestcontainers Node
GitHub Stars8,6324,8102,2052,521
LanguageJava/KotlinGoPythonTypeScript/JavaScript
First Release2016202020212020
Framework IntegrationJUnit 4/5, SpockTesting, GoConveypytest, unittestJest, Mocha, Vitest
Docker Compose SupportYesYesYesYes
Custom Container ImagesYesYesYesYes
Generic ContainersYesYesYesYes
Network SupportYesYesYesYes
Wait StrategiesLog, HTTP, Port, ShellLog, HTTP, PortLog, HTTP, PortLog, HTTP, Port
Reuse ContainersYesYesYesYes

Testcontainers Java: The Original

The Java implementation is the oldest and most mature. It offers the richest API with dedicated modules for PostgreSQL, MySQL, MongoDB, Elasticsearch, Kafka, Redis, and dozens more.

Installation

Add the dependency to your pom.xml or Gradle build:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.20.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.20.6</version>
    <scope>test</scope>
</dependency>

Usage Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Testcontainers
class UserRepositoryTest {

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

    @Test
    void testUserCreation() {
        String jdbcUrl = postgres.getJdbcUrl();
        String username = postgres.getUsername();
        String password = postgres.getPassword();
        
        // Use these credentials to connect to the real database
        DataSource ds = DataSourceBuilder.create()
            .url(jdbcUrl)
            .username(username)
            .password(password)
            .build();
        
        // Run your test against the real PostgreSQL instance
        UserRepository repo = new UserRepository(ds);
        repo.save(new User("alice", "alice@example.com"));
        assertEquals(1, repo.count());
    }
}

Docker Compose Integration

Java Testcontainers can also read an existing docker-compose.yml file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    ports:
      - "5432"
  redis:
    image: redis:7-alpine
    ports:
      - "6379"
1
2
3
4
5
@Container
static DockerComposeContainer<?> compose = new DockerComposeContainer<>(
    new File("src/test/resources/docker-compose-test.yml"))
    .withExposedService("postgres_1", 5432)
    .withExposedService("redis_1", 6379);

Testcontainers Go: Idiomatic Container Testing

The Go implementation follows Go conventions with a clean, chainable API. It supports generic containers as well as pre-built modules for PostgreSQL, MySQL, Redis, Kafka, and more.

Installation

1
2
go get github.com/testcontainers/testcontainers-go
go get github.com/testcontainers/testcontainers-go/modules/postgresql

Usage Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
    "context"
    "testing"
    
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
    "github.com/testcontainers/testcontainers-go/wait"
)

func TestPostgresContainer(t *testing.T) {
    ctx := context.Background()
    
    pgContainer, err := postgres.Run(ctx,
        "postgres:16-alpine",
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
        testcontainers.WithWaitStrategy(
            wait.ForLog("database system is ready to accept connections").
                WithOccurrence(2).
                WithStartupTimeout(30*time.Second)),
    )
    if err != nil {
        t.Fatal(err)
    }
    defer pgContainer.Terminate(ctx)
    
    connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
    if err != nil {
        t.Fatal(err)
    }
    
    // Connect to the real PostgreSQL instance
    db, err := sql.Open("postgres", connStr)
    defer db.Close()
    
    // Run your integration tests
    var count int
    db.QueryRow("SELECT 1").Scan(&count)
}

Docker Compose Support in Go

1
2
3
4
5
6
7
composeFilePath := "docker-compose-test.yml"
compose := tc.NewLocalDockerCompose(
    []string{composeFilePath},
    "test-compose",
)

execError := compose.WithCommand([]string{"up", "-d"}).Invoke()

Testcontainers Python: Pythonic Database Testing

The Python library integrates naturally with pytest and provides a clean context-manager API for managing container lifecycles.

Installation

1
pip install testcontainers[postgresql,mysql,redis]

Usage Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import pytest
from testcontainers.postgres import PostgresContainer
import psycopg2

def test_postgres_container():
    with PostgresContainer("postgres:16-alpine") as postgres:
        # Get connection details
        host = postgres.get_container_host_ip()
        port = postgres.get_exposed_port(5432)
        user = postgres.username
        password = postgres.password
        dbname = postgres.dbname
        
        # Connect to the real PostgreSQL instance
        conn = psycopg2.connect(
            host=host,
            port=port,
            user=user,
            password=password,
            dbname=dbname
        )
        
        cursor = conn.cursor()
        cursor.execute("CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100))")
        cursor.execute("INSERT INTO users (name) VALUES ('alice')")
        conn.commit()
        
        cursor.execute("SELECT COUNT(*) FROM users")
        count = cursor.fetchone()[0]
        assert count == 1
        
        cursor.close()
        conn.close()

Pytest Fixture Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import pytest
from testcontainers.mysql import MySqlContainer

@pytest.fixture(scope="module")
def mysql_container():
    with MySqlContainer("mysql:8.0") as mysql:
        yield mysql

def test_mysql_integration(mysql_container):
    conn_str = mysql_container.get_connection_url()
    # Use conn_str with SQLAlchemy, pymysql, etc.
    engine = create_engine(conn_str)
    with engine.connect() as conn:
        result = conn.execute(text("SELECT 1"))
        assert result.fetchone()[0] == 1

Testcontainers Node.js: JavaScript Integration Testing

The Node.js implementation works with Jest, Mocha, and Vitest. It provides both generic container support and pre-built modules for common databases.

Installation

1
npm install --save-dev testcontainers

Usage Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { GenericContainer, Wait } from "testcontainers";

describe("PostgreSQL Integration Tests", () => {
  let container: StartedTestContainer;

  beforeAll(async () => {
    container = await new GenericContainer("postgres:16-alpine")
      .withEnvironment({
        POSTGRES_DB: "testdb",
        POSTGRES_USER: "test",
        POSTGRES_PASSWORD: "test",
      })
      .withExposedPorts(5432)
      .withWaitStrategy(Wait.forLogMessage("database system is ready to accept connections"))
      .start();
  });

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

  it("should connect and query the database", async () => {
    const port = container.getMappedPort(5432);
    const host = container.getHost();
    
    const { Client } = require("pg");
    const client = new Client({
      host,
      port,
      user: "test",
      password: "test",
      database: "testdb",
    });
    
    await client.connect();
    const result = await client.query("SELECT 1 as value");
    expect(result.rows[0].value).toBe(1);
    await client.end();
  });
});

Docker Compose with Node.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { DockerComposeEnvironment } from "testcontainers";

describe("Compose Environment", () => {
  let environment: StartedDockerComposeEnvironment;

  beforeAll(async () => {
    environment = await new DockerComposeEnvironment(
      "/path/to/compose/dir",
      "docker-compose.yml"
    ).up();
  });

  afterAll(async () => {
    await environment.down();
  });
});

Why Use Container-Based Testing Over Mocks?

Mocks are fast but they lie. An in-memory H2 database behaves differently from PostgreSQL. A mocked HTTP client never experiences network timeouts. When you deploy to production, these differences cause bugs that your tests never caught.

Container-based testing gives you the best of both worlds: the accuracy of testing against real software and the isolation of per-test environments. Each test runs against a fresh, clean database with no state carried over from previous tests.

For database-heavy applications, this approach catches schema migration issues, query compatibility problems, and transaction isolation bugs that mocks simply cannot reproduce. See our database monitoring guide for production visibility, and backup verification strategies for ensuring your data survives failure scenarios.

Choosing the Right Testcontainers Language

ScenarioRecommended Library
Spring Boot / JPA applicationTestcontainers Java
Go HTTP API with PostgreSQLTestcontainers Go
Django / FastAPI Python serviceTestcontainers Python
Express.js / NestJS backendTestcontainers Node.js
Multi-language polyglot repoUse the library matching your test language
Docker Compose-based test fixturesAll four support Compose files

Each library shares the same core philosophy — spin up real containers, test against real services, clean up automatically — but exposes an API idiomatic to its host language. If your team writes tests in Java, use the Java library even if the service under test is in another language. The container runtime is the same Docker daemon.

FAQ

Do I need Docker installed to use Testcontainers?

Yes. All Testcontainers libraries communicate with the Docker daemon to create, manage, and destroy containers. Docker Desktop, Podman (with Docker socket compatibility), or a remote Docker host are all supported.

Can Testcontainers reuse containers across tests?

Yes. All four libraries support container reuse via a reuse() flag. This is useful for slow-starting containers like Elasticsearch, but be careful — reused containers carry state between tests, which can cause flaky tests if not managed properly.

How do I handle database migrations in Testcontainers?

Run your migration tool (Flyway, Liquibase, Alembic, Prisma, etc.) against the container after it starts but before your test queries execute. Most frameworks provide hooks or fixtures to run migrations at the right point in the test lifecycle.

Does Testcontainers work in CI/CD pipelines?

Yes. All four libraries work in GitHub Actions, GitLab CI, Jenkins, and other CI systems as long as Docker is available. For GitHub Actions, use docker: true in your runner or the setup-docker action. The Java library has the most battle-tested CI integrations, but Go, Python, and Node all work reliably.

Can I use Testcontainers for non-database testing?

Absolutely. Testcontainers supports Selenium/Chrome for browser testing, LocalStack for AWS service mocking, Kafka for message broker testing, and any custom Docker image via the Generic Container API. This makes it useful for end-to-end testing beyond just databases.

What happens if a container fails to start?

Each library implements wait strategies (log pattern matching, HTTP endpoint checks, port availability, or shell commands) that timeout after a configurable period. If the container fails to become ready within the timeout, the test fails with a clear error message rather than hanging indefinitely.