FastAPI Testing with pytest
Write fast, reliable tests for FastAPI apps using TestClient, pytest fixtures, dependency overrides, and a separate test database.
What you'll learn
- ✓How TestClient drives endpoints in-process
- ✓Designing fixtures for setup and teardown
- ✓Overriding dependencies for tests
- ✓Using a separate test database
- ✓Writing async tests with httpx
Prerequisites
- •Comfortable with FastAPI and basic pytest
What and Why
Testing a FastAPI app is fast and ergonomic. The provided TestClient runs your ASGI app in-process, so you skip the network and HTTP server entirely. Combine that with pytest fixtures and dependency overrides, and you can cover the full surface of your API in seconds.
Mental Model
TestClient wraps your app in httpx plus an ASGI transport. Each request goes through the real middleware, routing, and dependencies, but no socket is opened. Fixtures supply the test database session, fake users, and any other inputs.
pytest fixture
|
v
TestClient(app)
|
v
ASGI in-process
|
v
Middleware -> Routes -> Dependencies
|
v
Response object asserted in test Hands-on Example
A minimal app with a dependency.
# app.py
from fastapi import FastAPI, Depends
from pydantic import BaseModel
app = FastAPI()
def get_db():
# in production this yields a real session
yield {"users": {1: {"id": 1, "name": "Ada"}}}
class User(BaseModel):
id: int
name: str
@app.get("/users/{user_id}", response_model=User)
def read_user(user_id: int, db = Depends(get_db)):
return db["users"][user_id]
Tests with TestClient.
# tests/test_users.py
from fastapi.testclient import TestClient
from app import app, get_db
import pytest
@pytest.fixture
def client():
def fake_db():
yield {"users": {1: {"id": 1, "name": "TestAda"}}}
app.dependency_overrides[get_db] = fake_db
yield TestClient(app)
app.dependency_overrides.clear()
def test_read_user_ok(client):
r = client.get("/users/1")
assert r.status_code == 200
assert r.json() == {"id": 1, "name": "TestAda"}
def test_read_user_404(client):
r = client.get("/users/99")
assert r.status_code == 500 # in real code raise HTTPException(404)
Use a real test database with SQLAlchemy.
# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.db import Base, get_session
from app.main import app
from fastapi.testclient import TestClient
@pytest.fixture(scope="session")
def engine():
e = create_engine("sqlite+pysqlite:///:memory:", future=True)
Base.metadata.create_all(e)
return e
@pytest.fixture
def db(engine):
TestingSession = sessionmaker(bind=engine, autoflush=False, autocommit=False)
s = TestingSession()
try:
yield s
finally:
s.rollback()
s.close()
@pytest.fixture
def client(db):
def override():
yield db
app.dependency_overrides[get_session] = override
yield TestClient(app)
app.dependency_overrides.clear()
For async endpoints prefer httpx.AsyncClient.
import pytest, httpx
from httpx import ASGITransport
from app import app
@pytest.mark.asyncio
async def test_root():
async with httpx.AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as ac:
r = await ac.get("/")
assert r.status_code == 200
Common Pitfalls
- Forgetting to clear
app.dependency_overrides. State leaks across tests. - Sharing a database session between fixture and request. Always override
get_sessionso the endpoint sees the same session your test seeded. - Mocking too much. Tests that mock every dependency become tightly coupled to internals and miss real bugs.
- Slow setup. Recreating the schema for every test takes seconds at scale. Use a session-scoped engine and per-test transactions.
- Calling external services in tests. Mock the HTTP boundary with
respxor use a recorded fixture.
Practical Tips
- Group tests by route module. One file per resource keeps fixtures focused.
- Use parametrize to cover happy paths and error cases in one place.
- Snapshot test JSON responses for documentation endpoints.
- Run
pytest -x --ffwhile iterating to fail fast and rerun failures first. - Measure coverage with
pytest-covand treat dependency overrides as code you must cover.
Wrap-up
FastAPI is one of the easiest Python frameworks to test. TestClient runs in-process for speed, dependency overrides let you swap out the database and auth without monkeypatching, and pytest fixtures keep setup tidy. Build a small but solid harness once, and writing new tests becomes nearly free.
Related articles
- FastAPI FastAPI Authentication with JWT
Implement JWT-based authentication in FastAPI with OAuth2 password flow, secure token signing, and a reusable get_current_user dependency.
- FastAPI FastAPI CORS: A Practical Tutorial
Configure CORS in FastAPI without security holes: how the browser preflight works, which origins and headers to allow, credentials and cookies, and the most common misconfigurations to avoid.
- FastAPI FastAPI Deployment with Uvicorn and Gunicorn
Deploy FastAPI to production with Gunicorn managing Uvicorn workers. Cover process counts, timeouts, and health checks.
- FastAPI FastAPI Middleware Tutorial
Learn how FastAPI middleware works under the hood and write your own for logging, timing, and request enrichment.