Testing FastAPI apps with TestClient and pytest
Exercise endpoints without a running server, swap the database per test, write async tests with httpx, and assert the response shape rather than the whole body.
TestClient basics
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app.main import app
from app.db import get_db
from app.models import Base
engine = create_engine("sqlite://", connect_args={"check_same_thread": False},
poolclass=StaticPool)
@pytest.fixture
def client():
TestingSession = sessionmaker(bind=engine, autoflush=False)
Base.metadata.create_all(engine)
def override_get_db():
db = TestingSession()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as c: # context manager runs lifespan
yield c
app.dependency_overrides.clear()
Base.metadata.drop_all(engine)def test_create_and_read(client):
created = client.post("/api/v1/items", json={"name": "widget", "price": 9.99})
assert created.status_code == 201
body = created.json()
assert body["name"] == "widget"
fetched = client.get(f"/api/v1/items/{body['id']}")
assert fetched.status_code == 200
assert fetched.json()["price"] == 9.99Async tests with httpx
pip install pytest pytest-asyncio httpximport pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
@pytest.mark.anyio
async def test_health():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
r = await ac.get("/health")
assert r.status_code == 200
assert r.json() == {"status": "ok"}- TestClient is synchronous and wraps the app in a portal; it is the simplest option and covers most endpoints.
- httpx with
ASGITransportis genuinely async, which matters when you test concurrent behaviour or async dependencies. base_urlis required: httpx needs an absolute URL even though no socket is opened.
Assert shapes, not strings
import jsonschema
def test_response_matches_openapi_schema(client):
spec = client.get("/openapi.json").json()
schema = spec["components"]["schemas"]["ItemOut"]
r = client.get("/api/v1/items/1")
jsonschema.validate(r.json(), {**schema, "$ref": None})💡
Dependency overrides are stored on the app object, so they leak between tests unless you clear them. Set the override inside a fixture that also clears it — or the next test will quietly run against the previous test's database.
FAQ
Should tests hit a real database?
Run the bulk of your suite against SQLite for speed, and keep a smaller integration suite against the same engine you deploy (usually PostgreSQL). Behaviour differs: constraints, JSON columns and transaction semantics all diverge.
How do I test code that requires authentication?
Override the current-user dependency with a fixture that returns a known user, and keep one test that exercises the real token path. Overriding everywhere leaves the security logic untested.
Related
Dependencies and async Databases with SQLAlchemy and session dependencies
Last refreshed 2026-09-18.