Test containers with Docker in PyTest - Build an Automation Script
Start learning this pattern below
Jump into concepts and practice - no test required
import pytest import psycopg2 from testcontainers.postgres import PostgresContainer @pytest.fixture(scope='module') def postgres_container(): with PostgresContainer('postgres:15') as postgres: yield postgres def test_postgres_connection(postgres_container): conn = psycopg2.connect( dbname=postgres_container.POSTGRES_DB, user=postgres_container.POSTGRES_USER, password=postgres_container.POSTGRES_PASSWORD, host=postgres_container.get_container_host_ip(), port=postgres_container.get_exposed_port(postgres_container.port) ) cur = conn.cursor() cur.execute('CREATE TABLE test_table (id SERIAL PRIMARY KEY, name VARCHAR(50));') cur.execute("INSERT INTO test_table (name) VALUES ('testname') RETURNING id;") inserted_id = cur.fetchone()[0] conn.commit() cur.execute('SELECT name FROM test_table WHERE id = %s;', (inserted_id,)) result = cur.fetchone()[0] cur.close() conn.close() assert result == 'testname'
This test uses the testcontainers library to start a PostgreSQL Docker container automatically before the test and stop it after.
The postgres_container fixture manages the container lifecycle with scope='module' so it runs once per test module.
Inside the test, it connects to the database using connection info provided by the container object, avoiding hardcoded values.
It creates a table, inserts a record, and queries it back to verify the database is working.
Assertions check that the retrieved data matches the inserted data.
This approach ensures the test is isolated, repeatable, and cleans up resources properly.
Now add data-driven testing with 3 different names inserted and verified in the database
Practice
Solution
Step 1: Understand test containers purpose
Test containers run real services like databases inside Docker during tests.Step 2: Identify benefit in pytest context
This makes tests more reliable and realistic by using actual service environments.Final Answer:
They provide real service environments during tests for better reliability. -> Option DQuick Check:
Real service environment = Better test reliability [OK]
- Thinking test containers replace writing tests
- Believing they fix code bugs automatically
- Assuming tests run faster by skipping setup
Solution
Step 1: Check correct Docker client usage
Use docker.from_env() to get client, then client.containers.run() with detach=True to start container.Step 2: Verify fixture lifecycle management
Yield container for test, then stop container after test finishes.Final Answer:
def container(): client = docker.from_env() container = client.containers.run('redis', detach=True) yield container container.stop() -> Option CQuick Check:
Use client.containers.run with detach=True [OK]
- Calling client.run instead of client.containers.run
- Missing detach=True causing blocking call
- Not stopping container after test
import pytest
import docker
@pytest.fixture
def redis_container():
client = docker.from_env()
container = client.containers.run('redis:alpine', detach=True)
yield container
container.stop()
def test_redis_running(redis_container):
print(redis_container.status)Solution
Step 1: Understand container lifecycle in fixture
Container is started with detach=True, so status should be 'running' during test.Step 2: Check printed status in test
redis_container.status returns current container status, expected 'running' while test runs.Final Answer:
running -> Option AQuick Check:
Container started = status 'running' [OK]
- Expecting 'created' before container starts
- Assuming container is 'exited' during test
- Confusing status with container image tag
@pytest.fixture
def redis_container():
client = docker.from_env()
container = client.containers.run('redis', detach=True)
yield container
container.remove()Solution
Step 1: Check container cleanup steps
Container must be stopped before removal to avoid errors.Step 2: Identify missing stop call
Fixture calls container.remove() but misses container.stop() before it.Final Answer:
Missing container.stop() before container.remove() to stop container properly. -> Option AQuick Check:
Stop container before remove to clean up [OK]
- Calling remove without stopping container
- Confusing remove() with non-existent delete()
- Assuming environment vars are mandatory for container start
Solution
Step 1: Manage container lifecycle properly
Start PostgreSQL container detached to run in background during tests.Step 2: Implement readiness check before yielding
Poll container logs for 'database system is ready' message to ensure service is ready.Step 3: Yield container after readiness confirmed
This ensures tests run only after PostgreSQL is ready to accept connections.Final Answer:
Start container with detach=True, then poll container logs until 'database system is ready' appears before yielding. -> Option BQuick Check:
Wait for readiness log before yielding container [OK]
- Yielding container before it is ready
- Using fixed sleep instead of log polling
- Starting container without detach causing blocking
