Welcome back, aspiring Testcontainers pro! In the previous chapter, we explored the “why” behind Testcontainers – the pain points of traditional integration testing and how disposable environments offer a superior solution. Now, it’s time to get our hands dirty and witness the magic firsthand.
In this chapter, we’ll guide you through setting up your very first Testcontainer. Our mission? To programmatically spin up a real PostgreSQL database, use it in a test, and then let Testcontainers gracefully dispose of it. You’ll learn the core concepts of how Testcontainers interacts with Docker and see practical, step-by-step examples across Java, JavaScript/TypeScript, and Python. Get ready to banish those flaky tests and say “Hello, Postgres!” with confidence!
The Testcontainers Magic Trick: How It Works
Before we jump into code, let’s understand the fundamental steps Testcontainers takes to bring a containerized service to life for your tests. It’s not really magic, but rather a clever orchestration with Docker.
At its heart, Testcontainers acts as a friendly wrapper around your Docker daemon. When your test asks for a database (like PostgreSQL), here’s what happens:
- Request an Image: Your code tells Testcontainers, “Hey, I need a
postgres:latestcontainer!” - Docker Client in Action: Testcontainers, using its internal Docker client, communicates with your local (or remote) Docker daemon.
- Image Pull: If you don’t already have the
postgres:latestimage locally, Docker pulls it from Docker Hub (or your configured registry). - Container Launch: Docker launches a new container instance from that image. Testcontainers configures things like environment variables (e.g., database user/password), exposed ports, and mount points.
- Dynamic Port Mapping: Crucially, Testcontainers often maps internal container ports (like PostgreSQL’s default 5432) to random, available ports on your host machine. This prevents port conflicts when running multiple tests or multiple instances of the same service.
- Readiness Check: Testcontainers doesn’t just launch and assume the container is ready. It employs “wait strategies” – smart checks that wait until the service inside the container is fully up and functional (e.g., listening on its port, logs a specific message).
- Test Execution: Once the container is truly ready, Testcontainers provides you with the dynamically mapped host port and other connection details. Your test then connects to this real database instance and runs its assertions.
- Automatic Cleanup: When your test finishes (or the test suite completes, depending on configuration), Testcontainers signals Docker to stop and remove the container and its associated volume, leaving no trace behind. This is the “disposable” superpower!
This entire process ensures that each test run gets a clean, isolated environment, eliminating the headaches of shared development databases or complex mocking setups.
Here’s a simplified flow of this interaction:
Setting the Stage: Prerequisites
Before we dive into the code examples, make sure you have the following installed and running:
- Docker Desktop (or Docker Engine): This is non-negotiable! Testcontainers relies on Docker.
- Download from: https://docs.docker.com/get-docker/
- Verification: Open your terminal and run
docker run hello-world. If you see “Hello from Docker!”, you’re good to go.
- Your Preferred IDE: IntelliJ IDEA (Java), VS Code (JS/TS, Python), PyCharm (Python) are great choices.
- Language-Specific Tools:
- Java: JDK 17+ (LTS as of 2026-02-14, likely JDK 21 or 22), Maven (3.8+) or Gradle (8.x+).
- JavaScript/TypeScript: Node.js (v20+ LTS), npm (9.x+) or yarn (4.x+).
- Python: Python (3.9+), pip (23.x+), and optionally
venvfor virtual environments.
Your First Testcontainer: “Hello, Postgres!”
Now for the main event! We’ll demonstrate how to set up a PostgreSQL Testcontainer in Java, JavaScript/TypeScript, and Python. Pay attention to the similarities and differences in how each language’s Testcontainers library provides an idiomatic API.
Java Example (with JUnit 5)
We’ll use Testcontainers for Java with JUnit 5, which offers excellent integration via extensions.
1. Project Setup (Maven)
First, create a new Maven project (or add to an existing one). Add the following dependencies to your pom.xml:
<!-- pom.xml -->
<project>
<!-- ... other project configurations ... -->
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<junit.jupiter.version>5.10.1</junit.jupiter.version>
<testcontainers.version>1.19.4</testcontainers.version> <!-- Verified stable as of 2026-02-14 based on latest releases -->
<postgresql.driver.version>42.7.2</postgresql.driver.version>
</properties>
<dependencies>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<!-- Testcontainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<!-- PostgreSQL JDBC Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.driver.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<!-- ... build and plugin configurations ... -->
</project>
What did we add?
junit-jupiter-apiandjunit-jupiter-engine: For writing and running JUnit 5 tests.testcontainers: The core Testcontainers library.postgresql: A specific module for PostgreSQL, providing handy utilities.junit-jupiter: The JUnit 5 integration module that simplifies lifecycle management.postgresql(driver): The standard JDBC driver to connect to PostgreSQL.
2. Create Your First Test
Create a new Java class for your test, for example, src/test/java/com/example/PostgresTest.java:
// src/test/java/com/example/PostgresTest.java
package com.example;
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.assertTrue;
// 1. Mark the class with @Testcontainers to enable JUnit 5 integration
@Testcontainers
class PostgresTest {
// 2. Declare a static PostgreSQLContainer and annotate it with @Container
// Testcontainers will automatically start and stop this container for your tests.
// We specify the Docker image name and version.
@Container
public static PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>("postgres:15.5"); // Using a specific, recent LTS version
@Test
void testPostgresConnection() throws SQLException {
// 3. Get connection details from the running container
String jdbcUrl = postgresContainer.getJdbcUrl();
String username = postgresContainer.getUsername();
String password = postgresContainer.getPassword();
// Let's print them to see what Testcontainers generated!
System.out.println("JDBC URL: " + jdbcUrl);
System.out.println("Username: " + username);
System.out.println("Password: " + password);
// 4. Establish a connection using a standard JDBC driver
try (Connection connection = DriverManager.getConnection(jdbcUrl, username, password)) {
// 5. Create a statement and execute a simple query
try (Statement statement = connection.createStatement()) {
ResultSet resultSet = statement.executeQuery("SELECT 1");
// 6. Assert that we got a result, confirming the connection is live
assertTrue(resultSet.next(), "Should get a result from the database.");
assertTrue(resultSet.getInt(1) == 1, "The result should be 1.");
}
}
}
}
What’s happening here?
@Testcontainers: This JUnit 5 extension tells JUnit that this test class uses Testcontainers. It’s responsible for managing the lifecycle of@Containerannotated fields.@Container: This annotation on ourpostgresContainerfield is the magic. When the test class is initialized, Testcontainers sees this and automatically starts a newpostgres:15.5Docker container before any tests run.PostgreSQLContainer<?>("postgres:15.5"): We instantiate aPostgreSQLContainer, which is a specializedGenericContainerfor PostgreSQL. It comes with built-in wait strategies and default configurations (like default usernametest, passwordtest, and databasetest). We’re usingpostgres:15.5, a recent stable version.postgresContainer.getJdbcUrl(),getUsername(),getPassword(): These methods are provided by thePostgreSQLContainerto easily get the dynamically mapped port and pre-configured credentials needed to connect to the database from your host machine.DriverManager.getConnection(...): This is standard JDBC code. You’re connecting to a real PostgreSQL instance, just like you would connect to any other database, but this one is temporary and managed by Testcontainers!assertTrue(resultSet.next()): This assertion simply checks if the database returned a row, confirming our connection and query worked.
Run this test, and you’ll see a PostgreSQL container briefly appear in your Docker Desktop, run the test, and then disappear. Amazing!
JavaScript/TypeScript Example (with Jest)
For Node.js environments, we use the testcontainers-node library, often with a testing framework like Jest.
1. Project Setup
Create a new directory, initialize a Node.js project, and install dependencies:
mkdir testcontainers-nodejs-example
cd testcontainers-nodejs-example
npm init -y # or yarn init -y
npm install --save-dev jest typescript ts-node @types/node @types/jest testcontainers pg @types/pg # or yarn add -D ...
npx ts-jest config:init # if using TypeScript and Jest
Make sure your tsconfig.json (if using TypeScript) is configured correctly, for instance, target: "es2022", module: "commonjs".
2. Create Your First Test
Create a test file, e.g., src/test/postgres.test.ts:
// src/test/postgres.test.ts
import { GenericContainer, StartedTestContainer, Wait } from "testcontainers";
import { Client } from "pg"; // Node.js PostgreSQL client
describe("PostgreSQL Testcontainers Example", () => {
let postgresContainer: StartedTestContainer; // Declare a variable to hold our container instance
let pgClient: Client;
// Before all tests in this suite, we'll start our container
beforeAll(async () => {
console.log("Starting PostgreSQL container...");
// 1. Create a GenericContainer instance, specifying the Docker image
// We're using 'postgres:15.5' for a recent LTS version
postgresContainer = await new GenericContainer("postgres:15.5")
// 2. Expose the default PostgreSQL port
.withExposedPorts(5432)
// 3. Set environment variables for the database (user, password, db name)
.withEnv({
POSTGRES_USER: "myuser",
POSTGRES_PASSWORD: "mypassword",
POSTGRES_DB: "mydatabase"
})
// 4. Set a wait strategy to ensure the database is ready
// We wait for a specific log message indicating readiness
.withWaitStrategy(Wait.forLogMessage("database system is ready to accept connections", 2))
// 5. Start the container!
.start();
// 6. Get the dynamically mapped port from the container
const mappedPort = postgresContainer.getMappedPort(5432);
const host = postgresContainer.getHost();
// Log connection details for debugging/observation
console.log(`PostgreSQL running on ${host}:${mappedPort}`);
console.log(`DB Name: mydatabase, User: myuser, Password: mypassword`);
// 7. Initialize the PostgreSQL client
pgClient = new Client({
host: host,
port: mappedPort,
user: "myuser",
password: "mypassword",
database: "mydatabase"
});
// 8. Connect to the database
await pgClient.connect();
console.log("Connected to PostgreSQL successfully!");
}, 60000); // Give it up to 60 seconds to start and connect
// After all tests, we'll stop and remove the container
afterAll(async () => {
if (pgClient) {
await pgClient.end();
console.log("Disconnected from PostgreSQL.");
}
if (postgresContainer) {
await postgresContainer.stop();
console.log("PostgreSQL container stopped.");
}
});
it("should be able to connect and query PostgreSQL", async () => {
// 9. Execute a simple query
const result = await pgClient.query("SELECT 1 AS result_value");
// 10. Assert the result
expect(result.rows[0].result_value).toBe(1);
console.log("Query 'SELECT 1' successful.");
});
});
What’s happening here?
GenericContainer("postgres:15.5"): In Node.js, we often useGenericContainerfor any service. We pass the Docker image name and tag..withExposedPorts(5432): We tell Testcontainers that port 5432 inside the container is relevant, so it should map it to a random host port..withEnv({...}): We set environment variables that the PostgreSQL image understands to configure the database, user, and password..withWaitStrategy(Wait.forLogMessage(...)): This is crucial! It tells Testcontainers to wait until PostgreSQL emits a specific log message indicating it’s ready. Without this, your test might try to connect before the database is fully initialized.await ... .start(): Asynchronous operation to launch the container.postgresContainer.getMappedPort(5432)andgetHost(): These methods retrieve the dynamically assigned host port and the host IP (usuallylocalhost) that you’ll use to connect.pgClient = new Client(...): We initialize thepgclient (a popular Node.js PostgreSQL driver) with the details obtained from Testcontainers.beforeAllandafterAll: Jest hooks are used to manage the container’s lifecycle:beforeAllstarts it, andafterAllstops it, ensuring it’s available for all tests in the suite and then cleaned up.
Run your test using npm test or npx jest src/test/postgres.test.ts.
Python Example (with pytest)
For Python, the testcontainers-python library integrates seamlessly with pytest, often leveraging fixtures.
1. Project Setup
Create a new directory, initialize a virtual environment, and install dependencies:
mkdir testcontainers-python-example
cd testcontainers-python-example
python3 -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install pytest testcontainers psycopg2-binary
2. Create Your First Test
Create a test file, e.g., tests/test_postgres.py:
# tests/test_postgres.py
import pytest
from testcontainers.postgres import PostgreSQLContainer
import psycopg2
# 1. Define a pytest fixture that provides a PostgreSQLContainer
# This fixture will be executed once for the test session (scope="session")
# or for each test function (scope="function") if not specified.
# "session" scope is generally better for performance to reuse the container.
@pytest.fixture(scope="session")
def postgres_container():
# 2. Instantiate PostgreSQLContainer, specifying the Docker image
# We're using 'postgres:15.5' for a recent LTS version
# The default username, password, and database are 'test' for PostgreSQLContainer
with PostgreSQLContainer("postgres:15.5") as postgres:
# 3. Testcontainers handles starting the container and waiting for it to be ready
# The 'with' statement ensures the container is stopped and removed automatically
postgres.start()
print(f"\nPostgreSQL container started on {postgres.get_container_host_ip()}:{postgres.get_exposed_port(5432)}")
print(f"Connection string: {postgres.get_connection_url()}")
yield postgres # Yield the container instance to the tests that use this fixture
# 4. Define another fixture to provide a database connection
@pytest.fixture(scope="function")
def pg_connection(postgres_container):
# 5. Get the connection URL from the running container
connection_url = postgres_container.get_connection_url()
# Connect using psycopg2
conn = psycopg2.connect(connection_url)
try:
yield conn # Yield the connection object to the test
finally:
conn.close() # Ensure the connection is closed after the test
# 6. Write your test function, using the fixtures
def test_postgres_select_1(pg_connection):
cursor = pg_connection.cursor()
cursor.execute("SELECT 1")
result = cursor.fetchone()[0] # Fetch the result
assert result == 1 # Assert that the result is 1
print("Query 'SELECT 1' successful.")
cursor.close()
What’s happening here?
@pytest.fixture(scope="session"): This decorator definespostgres_containeras a pytest fixture.scope="session"means this container will be started once for the entire test session and reused across all tests that need it, and then stopped when all tests are done. This is often an optimization for performance.with PostgreSQLContainer("postgres:15.5") as postgres:: This Pythonicwithstatement is a fantastic feature. It creates thePostgreSQLContainerinstance, and the__enter__method implicitly callsstart(). Crucially, when thewithblock exits (after all tests using the fixture are done), the__exit__method is called, which handlesstop()and cleanup automatically.postgres.get_connection_url(): ThePostgreSQLContainerprovides a convenient method to get a full connection string (including host, mapped port, user, password, database) for thepsycopg2driver.yield postgres: In a pytest fixture,yieldpasses the value (our running container) to the test function. Execution then resumes from here when the test finishes.pg_connection(postgres_container): This second fixture demonstrates how you can chain fixtures. It automatically receives thepostgres_containerinstance and uses it to establish apsycopg2connection.test_postgres_select_1(pg_connection): Our actual test function now simply receives a ready-to-use database connection from thepg_connectionfixture and can execute queries.
Run your test using pytest tests/test_postgres.py.
Mini-Challenge: Extend Your PostgreSQL Test
Now that you’ve seen the basics, it’s your turn to practice!
Challenge:
Modify the Python example (tests/test_postgres.py) to do the following:
- Create a table: After connecting to the database, execute a SQL statement to create a simple table named
userswith columnsid(INT, PRIMARY KEY) andname(VARCHAR(255)). - Insert data: Insert one or two rows into the
userstable. - Query data: Select all data from the
userstable and assert that you retrieve the data you just inserted.
Hint:
- Remember to
commit()your changes afterINSERTstatements if your connection is in auto-commit mode (whichpsycopg2connection typically is not by default). - You’ll need
cursor.execute()forCREATE TABLEandINSERT, andcursor.fetchall()for retrieving multiple rows.
What to Observe/Learn:
- You are interacting with a real database instance, not a mock.
- Each time you run the test (if using
scope="function"for the container or if you stop/restart the session forscope="session"), you get a clean database. This ensures your tests are isolated and repeatable.
💡 **Need a little help? Click for a hint!**
When defining your `pg_connection` fixture, you can execute SQL commands right after `conn = psycopg2.connect(connection_url)` and before `yield conn` to set up your schema. Make sure to call `conn.commit()` after your DDL/DML statements. In your test, you'll open a new cursor to perform the `SELECT` query.Common Pitfalls & Troubleshooting
Even with Testcontainers’ elegance, you might encounter some bumps. Here are common issues and how to tackle them:
“Cannot connect to the Docker daemon” / Docker Not Running:
- Symptom: You’ll see an error like
org.testcontainers.dockerclient.DockerClientException: Could not connect to Docker daemon(Java) or similar messages in other languages. - Fix: Ensure Docker Desktop (or your Docker engine) is running. Check your system tray (Windows/macOS) or run
docker infoin your terminal. If it’s not running, start it up!
- Symptom: You’ll see an error like
Image Pull Issues / Network Problems:
- Symptom: Testcontainers tries to pull an image but times out or fails with a network error.
- Fix: Check your internet connection. Ensure there are no corporate proxies or firewalls blocking Docker Hub access. You might need to configure Docker’s proxy settings. Also, double-check the image name and tag for typos (e.g.,
postgres:15.5notpostgress:15.5).
Container Startup Timeout /
WaitStrategyIssues:- Symptom: Your test hangs for a long time (e.g., 60 seconds) and then fails with a message indicating the container didn’t become ready within the timeout period.
- Fix:
- Check container logs: Testcontainers usually prints the container logs on failure. Look for clues: Did the service crash? Is there an unexpected configuration error?
- Review
WaitStrategy: ForGenericContainer(especially in JS/TS), ensure yourWait.forLogMessage()or other wait strategy correctly identifies when the service is fully operational. Sometimes logs change between image versions. - Increase timeout: As a last resort, you can increase the default startup timeout (e.g.,
withStartupTimeout(Duration.ofSeconds(120))in Java or60000in JestbeforeAll).
Connection Refused / Incorrect Port Mapping:
- Symptom: Your application code tries to connect to the database, but gets a “connection refused” error.
- Fix:
- Always use mapped ports: Remember, Testcontainers maps internal container ports to random host ports. Never hardcode
5432forlocalhostunless you’re explicitly binding a fixed port (which is generally discouraged). Always usecontainer.getMappedPort(internalPort)(Java/JS) orcontainer.get_exposed_port(internalPort)(Python). - Check host IP: For some network configurations,
localhostmight not resolve correctly inside the test environment. Testcontainers provides methods likecontainer.getHost()(Java/JS) orcontainer.get_container_host_ip()(Python) to get the correct host IP for connection.
- Always use mapped ports: Remember, Testcontainers maps internal container ports to random host ports. Never hardcode
Summary
Congratulations! You’ve successfully launched your first Testcontainer and connected to a real PostgreSQL database, demonstrating the power of disposable, on-demand test environments.
Here are the key takeaways from this chapter:
- Testcontainers’ Core Workflow: You learned how Testcontainers orchestrates Docker to pull images, start containers, map ports dynamically, check for readiness, and perform automatic cleanup.
- Real vs. Mock: We reinforced the concept that Testcontainers provides real service instances, not mocks or fakes, leading to higher confidence in your integration tests.
- Multi-Language Examples: You’ve seen idiomatic implementations for starting a PostgreSQL container in Java (JUnit 5), JavaScript/TypeScript (Jest), and Python (pytest), highlighting both the similarities in approach and differences in API.
- Hands-on Practice: The mini-challenge allowed you to apply your new knowledge and interact with the database directly.
- Troubleshooting Basics: You’re now equipped to handle common setup and connection issues.
In the next chapter, we’ll dive deeper into configuring Testcontainers, exploring different container types beyond databases, and learning about more advanced wait strategies and container networking. Get ready to expand your Testcontainers toolkit!
References
- Testcontainers Official Documentation: https://testcontainers.com/
- Testcontainers-Java GitHub Releases: https://github.com/testcontainers/testcontainers-java/releases
- Testcontainers-Node GitHub Releases: https://github.com/testcontainers/testcontainers-node/releases
- Testcontainers-Python GitHub Releases: https://github.com/testcontainers/testcontainers-python/releases
- Docker Get Started: https://docs.docker.com/get-started/
- JUnit 5 User Guide: https://junit.org/junit5/docs/current/user-guide/
- Jest Documentation: https://jestjs.io/docs/
- Pytest Documentation: https://docs.pytest.org/
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.