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.99

Async tests with httpx

pip install pytest pytest-asyncio httpx
import 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 ASGITransport is genuinely async, which matters when you test concurrent behaviour or async dependencies.
  • base_url is 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.

Dependencies and async Databases with SQLAlchemy and session dependencies

Last refreshed 2026-09-18.