Cheatsheet for testing the integrated stack.
conftest.py
import pytest, pytest_asyncio
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy import event
from testcontainers.postgres import PostgresContainer
from src.myapp.main import app
from src.myapp.db import Base
from src.myapp.deps import get_db
@pytest.fixture(scope="session")
def anyio_backend():
return "asyncio"
@pytest.fixture(scope="session")
def postgres():
with PostgresContainer("postgres:17") as pg:
yield pg
@pytest_asyncio.fixture(scope="session")
async def engine(postgres):
url = postgres.get_connection_url().replace("psycopg2", "asyncpg")
eng = create_async_engine(url)
async with eng.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield eng
await eng.dispose()
@pytest_asyncio.fixture
async def db_session(engine):
connection = await engine.connect()
transaction = await connection.begin()
async with AsyncSession(bind=connection, expire_on_commit=False) as session:
nested = await session.begin_nested()
@event.listens_for(session.sync_session, "after_transaction_end")
def end_savepoint(session, transaction):
nonlocal nested
if not nested.is_active:
nested = session.begin_nested()
yield session
await transaction.rollback()
await connection.close()
@pytest_asyncio.fixture
async def client(db_session):
async def override_db():
yield db_session
app.dependency_overrides[get_db] = override_db
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
yield ac
app.dependency_overrides.clear()
Per-test savepoint; full rollback at end.
Factories
import factory
from src.myapp.models import User
class UserFactory(factory.Factory):
class Meta:
model = User
email = factory.Sequence(lambda n: f"user{n}@example.com")
hashed_password = "fake-hash"
is_active = True
Happy path test
@pytest.mark.anyio
async def test_create_user(client):
r = await client.post("/users", json={
"email": "[email protected]",
"password": "abcd1234",
})
assert r.status_code == 201
body = r.json()
assert body["email"] == "[email protected]"
assert "password" not in body
Validation test
@pytest.mark.anyio
@pytest.mark.parametrize("payload, status", [
({"email": "[email protected]", "password": "abcd1234"}, 201),
({"email": "invalid", "password": "abcd1234"}, 422),
({"email": "[email protected]", "password": "short"}, 422),
({"email": "[email protected]"}, 422),
])
async def test_validation(client, payload, status):
r = await client.post("/users", json=payload)
assert r.status_code == status
Auth flow test
@pytest.mark.anyio
async def test_login_flow(client, db_session):
user = UserFactory.build(hashed_password=hash_password("abcd1234"))
db_session.add(user); await db_session.commit()
r = await client.post("/login", data={"username": user.email, "password": "abcd1234"})
assert r.status_code == 200
token = r.json()["access_token"]
r = await client.get("/me", headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 200
assert r.json()["email"] == user.email
DB constraint test
@pytest.mark.anyio
async def test_email_unique(client, db_session):
UserFactory.build(email="[email protected]").__dict__
db_session.add(UserFactory.build(email="[email protected]"))
await db_session.commit()
r = await client.post("/users", json={"email": "[email protected]", "password": "abcd1234"})
assert r.status_code == 409
Drift test
@pytest.mark.anyio
async def test_no_drift(engine):
from alembic.autogenerate import compare_metadata
from alembic.migration import MigrationContext
async with engine.connect() as conn:
def check(sync_conn):
ctx = MigrationContext.configure(sync_conn)
return compare_metadata(ctx, Base.metadata)
diff = await conn.run_sync(check)
assert diff == [], f"Drift: {diff}"
Migration up/down round-trip
def test_migrations_round_trip(postgres):
from alembic.config import Config
from alembic import command
cfg = Config("alembic.ini")
cfg.set_main_option("sqlalchemy.url", postgres.get_connection_url())
command.upgrade(cfg, "head")
command.downgrade(cfg, "base")
command.upgrade(cfg, "head")
Mocking external services
import respx
@pytest_asyncio.fixture
async def mock_stripe():
with respx.mock(base_url="https://api.stripe.com") as m:
m.post("/v1/charges").respond(json={"id": "ch_test"})
yield m
@pytest.mark.anyio
async def test_charge(client, mock_stripe):
r = await client.post("/payments", json={...})
assert r.status_code == 200
assert mock_stripe.calls.call_count == 1
Auth fixture
@pytest_asyncio.fixture
async def admin_client(client, db_session):
admin = UserFactory.build(role="admin")
db_session.add(admin); await db_session.commit()
token = make_access_token(admin.id)
client.headers["Authorization"] = f"Bearer {token}"
yield client
Coverage
pytest --cov=src --cov-report=term --cov-fail-under=80
CI integration
services:
postgres:
image: postgres:17
env: { POSTGRES_PASSWORD: test }
ports: ["5432:5432"]
steps:
- run: uv sync --frozen
- run: uv run alembic upgrade head
- run: uv run pytest --cov=src
Common mistakes
- Mocking the DB instead of using a real one — false confidence.
- Sharing state across tests — flaky.
- Forgetting to clear
dependency_overrides. - No drift test — models drift unnoticed.
Read this next
If you want my full pytest harness + factories + drift CI, 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 .