Docker for Test Environments¶
Running services under test in Docker containers: compose files for local stacks, testcontainers for programmatic lifecycle, and CI integration patterns.
Docker Compose for Test Stack¶
# docker-compose.test.yml
version: "3.8"
services:
app:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://test:test@db:5432/testdb
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test"]
interval: 5s
timeout: 3s
retries: 5
ports:
- "5432:5432"
# Start stack, run tests, tear down
docker compose -f docker-compose.test.yml up -d --wait
pytest tests/
docker compose -f docker-compose.test.yml down -v
Testcontainers (Programmatic)¶
from testcontainers.postgres import PostgresContainer
@pytest.fixture(scope="session")
def postgres():
with PostgresContainer("postgres:16") as pg:
yield pg
@pytest.fixture(scope="session")
def db_url(postgres):
return postgres.get_connection_url()
def test_user_crud(db_url):
engine = create_engine(db_url)
# test against real PostgreSQL
Testcontainers starts a real Docker container per fixture, tears it down after.
Multi-Service Testing¶
from testcontainers.compose import DockerCompose
@pytest.fixture(scope="session")
def compose():
with DockerCompose(".", compose_file_name="docker-compose.test.yml") as c:
c.wait_for("http://localhost:8000/health")
yield c
Building Test Images¶
# Dockerfile.test
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["pytest", "--tb=short", "-v"]
Wait Strategies¶
Never assume services are ready after docker compose up:
import time
import requests
from requests.exceptions import ConnectionError
def wait_for_service(url, timeout=30, interval=1):
deadline = time.time() + timeout
while time.time() < deadline:
try:
resp = requests.get(url)
if resp.status_code == 200:
return
except ConnectionError:
pass
time.sleep(interval)
raise TimeoutError(f"Service {url} not ready after {timeout}s")
Volume Mounts for Test Data¶
Read-only mounts (:ro) prevent tests from accidentally modifying source data.
CI Integration¶
# .github/workflows/test.yml
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: pytest
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
Gotchas¶
-
Issue: Tests flaky because service not ready despite
depends_on. Fix: Dockerdepends_ononly waits for container start, not application readiness. Always use healthchecks or explicit wait-for-port logic. -
Issue: Port conflicts when running tests locally and in CI simultaneously. Fix: Use dynamic port mapping:
ports: ["0:5432"]and read assigned port from Docker. Or use testcontainers which handles this automatically. -
Issue: Docker volumes persist data between test runs, causing test pollution. Fix: Use
docker compose down -vto remove volumes. Or usetmpfsmounts:tmpfs: /var/lib/postgresql/data.