Cheatsheet for testing. Long-form: Textbook Ch 10 .

Setup

uv add --dev pytest pytest-anyio httpx pytest-cov factory_boy testcontainers respx freezegun

conftest.py (async client)

import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app

@pytest.fixture(scope="session")
def anyio_backend(): return "asyncio"

@pytest.fixture
async def client():
    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
        yield c

Basic test

import pytest

@pytest.mark.anyio
async def test_root(client):
    r = await client.get("/")
    assert r.status_code == 200

Dependency overrides

def fake_db(): return FakeDB()

@pytest.fixture
async def client(db_session):
    app.dependency_overrides[get_db] = lambda: db_session
    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
        yield c
    app.dependency_overrides.clear()

Real DB via testcontainers

from testcontainers.postgres import PostgresContainer
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession

@pytest.fixture(scope="session")
def postgres():
    with PostgresContainer("postgres:17") as pg:
        yield pg

@pytest.fixture(scope="session")
async def engine(postgres):
    url = postgres.get_connection_url().replace("psycopg2", "asyncpg")
    e = create_async_engine(url)
    async with e.begin() as c:
        await c.run_sync(Base.metadata.create_all)
    yield e
    await e.dispose()

@pytest.fixture
async def db_session(engine):
    conn = await engine.connect()
    tx = await conn.begin()
    async with AsyncSession(bind=conn, expire_on_commit=False) as s:
        yield s
    await tx.rollback()
    await conn.close()

Per-test transaction rolls back on teardown. Fast + isolated.

factory_boy

import factory

class UserFactory(factory.Factory):
    class Meta:
        model = User
    email = factory.Sequence(lambda n: f"u{n}@example.com")
    full_name = factory.Faker("name")
    is_active = True

# In a test
async def test_create(db_session):
    u = UserFactory.build()
    db_session.add(u)
    await db_session.commit()
    assert u.id

Auth client

@pytest.fixture
async def auth_client(client, db_session):
    user = UserFactory.build(); db_session.add(user); await db_session.commit()
    token = make_test_token(user.id)
    client.headers["Authorization"] = f"Bearer {token}"
    yield client

# Or via override
@pytest.fixture
def auth_client(client):
    user = User(id=1, email="t@x", role="admin")
    app.dependency_overrides[current_user] = lambda: user
    yield client
    app.dependency_overrides.pop(current_user, None)

Mock external HTTP (respx)

import respx

@pytest.fixture
def mock_external():
    with respx.mock(base_url="https://upstream.api") as m:
        m.get("/users/1").respond(json={"id": 1})
        yield m

async def test_proxy(client, mock_external):
    r = await client.get("/proxy/users/1")
    assert mock_external.calls.call_count == 1

Freeze time

from freezegun import freeze_time

@freeze_time("2026-01-01")
async def test_today(client):
    r = await client.get("/today")
    assert r.json()["d"] == "2026-01-01"

Parametrize

@pytest.mark.parametrize("payload, code", [
    ({"email": "[email protected]", "name": "x"}, 201),
    ({"email": "bad"}, 422),
    ({}, 422),
])
async def test_validation(client, payload, code):
    r = await client.post("/users", json=payload)
    assert r.status_code == code

WebSocket test

from fastapi.testclient import TestClient

def test_ws():
    with TestClient(app).websocket_connect("/ws") as ws:
        ws.send_json({"hello": "world"})
        assert ws.receive_json() == {"echo": {"hello": "world"}}

OpenAPI snapshot test

def test_openapi():
    s = app.openapi()
    assert "/users" in s["paths"]
    assert "post" in s["paths"]["/users"]

Settings override (env)

@pytest.fixture(autouse=True)
def settings(monkeypatch):
    monkeypatch.setenv("MYAPP_DATABASE_URL", "postgresql://...")
    monkeypatch.setenv("MYAPP_SECRET_KEY", "test")

Coverage

pytest --cov=src --cov-report=term --cov-report=html --cov-fail-under=80

CI (GitHub Actions)

- uses: actions/setup-python@v5
  with: { python-version: "3.13" }
- run: uv sync --frozen
- run: uv run pytest --cov=src --cov-fail-under=80
services:
  postgres:
    image: postgres:17
    env: { POSTGRES_PASSWORD: test }
    ports: ["5432:5432"]
    options: --health-cmd "pg_isready"

Read this next

If you want my FastAPI test harness (testcontainers + factories + asyncclient), it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .