testcontainers-python is actually a wrapper around the Java testcontainers library, which is the core engine for managing container lifecycles and exposing their ports to your local test environment.
Here’s how you can spin up a PostgreSQL database using testcontainers-python and run some integration tests against it.
import pytest
import docker
import testcontainers.postgres
import psycopg2
@pytest.fixture(scope="module")
def postgres_container():
# Define the Docker image to use
image = "postgres:13"
# Create a PostgreSQL container instance
# The `docker.services.latest()` finds the latest version of the PostgreSQL image
# `container_kwargs` allows passing specific Docker container configuration
# `ports` maps a host port to the container's default PostgreSQL port (5432)
with testcontainers.postgres.PostgresContainer(image=image) as postgres:
# Wait until the container is ready and the database is accessible
# `get_connection_url()` returns a string like:
# "postgresql://user:password@host:port/database"
connection_url = postgres.get_connection_url()
yield connection_url
@pytest.fixture(scope="module")
def db_connection(postgres_container):
# Extract connection details from the URL
from urllib.parse import urlparse
result = urlparse(postgres_container)
db_params = {
"database": result.path[1:],
"user": result.username,
"password": result.password,
"host": result.hostname,
"port": result.port,
}
# Establish a connection to the PostgreSQL database
conn = psycopg2.connect(**db_params)
yield conn
conn.close()
def test_database_connection(db_connection):
"""Tests if we can successfully connect to the PostgreSQL database."""
assert db_connection is not None
assert not db_connection.closed
def test_create_and_query_table(db_connection):
"""Tests creating a table, inserting data, and querying it."""
cursor = db_connection.cursor()
# Create a sample table
cursor.execute("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(50));")
db_connection.commit()
# Insert data
cursor.execute("INSERT INTO users (name) VALUES (%s);", ("Alice",))
cursor.execute("INSERT INTO users (name) VALUES (%s);", ("Bob",))
db_connection.commit()
# Query data
cursor.execute("SELECT name FROM users WHERE name = %s;", ("Alice",))
result = cursor.fetchone()
assert result is not None
assert result[0] == "Alice"
cursor.execute("SELECT COUNT(*) FROM users;")
count = cursor.fetchone()[0]
assert count == 2
cursor.close()
When you run pytest in your terminal in the same directory as this file, testcontainers-python will:
- Pull the
postgres:13Docker image if it’s not already present on your system. - Start a new PostgreSQL container from that image.
- Expose the PostgreSQL port (5432) to a random available port on your host machine.
- Wait for the PostgreSQL service inside the container to become ready (i.e., accept connections).
- Generate a connection URL that points to this dynamically assigned host port.
- Pass this connection URL to your
postgres_containerfixture. - Your
db_connectionfixture then uses this URL to establish apsycopg2connection. - Your tests (
test_database_connection,test_create_and_query_table) execute against this live, isolated PostgreSQL instance. - Once all tests in the module are complete, the container is automatically stopped and removed.
This provides a clean, isolated, and reproducible environment for your integration tests, ensuring they don’t interfere with each other or your local development database.
The core mechanism behind testcontainers is its ability to orchestrate Docker containers. It leverages the Docker SDK to pull images, create containers, manage their lifecycle (start, stop, remove), and crucially, to map container ports to host ports. When you request a port from the container (like PostgreSQL’s 5432), testcontainers asks the Docker daemon to allocate an ephemeral port on your host and forward traffic from that host port to the container’s port. This dynamic port mapping is key because it avoids port conflicts with services already running on your host or other containers. The library then provides convenient methods to retrieve these mapped host ports or full connection URLs, abstracting away the low-level Docker API calls.
A common pitfall is forgetting to commit() transactions in your database tests. If you INSERT or CREATE TABLE and don’t commit, the changes won’t be persisted within the container’s database session, and subsequent queries will appear to fail or return no data, even though your code logic is correct. The db_connection.commit() call ensures that the changes made by your test are finalized within the running PostgreSQL instance before your test proceeds or finishes.
The next step is to explore how to manage multiple containers for more complex scenarios, such as testing an application that depends on both a database and a message queue.