FastAPI testing is one of the friendliest stories in Python web frameworks. We don’t spin up a real server — we use an in-process client that calls our app directly. Fast, deterministic, no port conflicts.
TestClient — the workhorse
TestClient is a thin wrapper around httpx that talks to our app via ASGI in-process. We write tests as if we were making real HTTP calls.
pip install pytest httpx
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health():
return {"status": "ok"}
@app.post("/items")
def create_item(item: dict):
return {"id": 1, **item}
# test_main.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_health():
r = client.get("/health")
assert r.status_code == 200
assert r.json() == {"status": "ok"}
def test_create_item():
r = client.post("/items", json={"name": "Book", "price": 10})
assert r.status_code == 200
body = r.json()
assert body["id"] == 1
assert body["name"] == "Book"
Run: pytest. That’s it. No server, no fixtures needed.
TestClient works for sync and async routes both — it handles the event loop internally. So even if our handler is async def, we still use the regular sync TestClient.
When to reach for httpx.AsyncClient
If we want our test code to be async (e.g., the test awaits other async things like a real Redis client or async DB session), use httpx.AsyncClient with ASGITransport.
import pytest
import httpx
from httpx import ASGITransport
from main import app
@pytest.mark.asyncio
async def test_health_async():
transport = ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as ac:
r = await ac.get("/health")
assert r.status_code == 200
Needs pytest-asyncio (or anyio plugin). Use this when the test itself needs to await something.
Dependency overrides — the killer feature
This is the single best thing about FastAPI testing. Any dependency in our app can be swapped at test time.
# main.py
from fastapi import Depends, FastAPI
def get_db():
db = RealDB()
try:
yield db
finally:
db.close()
def get_current_user():
# reads JWT from header, hits DB
...
app = FastAPI()
@app.get("/me")
def me(user = Depends(get_current_user), db = Depends(get_db)):
return db.get_profile(user.id)
In the test, we override:
# test_main.py
from main import app, get_db, get_current_user
class FakeDB:
def get_profile(self, user_id):
return {"id": user_id, "name": "Test User"}
def fake_user():
return type("User", (), {"id": 1})()
app.dependency_overrides[get_db] = lambda: FakeDB()
app.dependency_overrides[get_current_user] = fake_user
def test_me():
r = client.get("/me")
assert r.json()["name"] == "Test User"
# clean up afterwards
def teardown_module():
app.dependency_overrides.clear()
In simple language: we don’t have to mock anything inside the function. We tell FastAPI “when this dependency runs, use this fake instead”. No monkeypatch, no mock.patch, no awkward wiring.
This is what makes FastAPI apps so testable — keep DB sessions, auth, external API clients, etc. behind dependencies, and tests can swap them out trivially.
Fixtures — cleaner setup
A typical conftest.py:
import pytest
from fastapi.testclient import TestClient
from main import app, get_db, get_current_user
@pytest.fixture
def client():
app.dependency_overrides[get_db] = lambda: FakeDB()
app.dependency_overrides[get_current_user] = fake_user
with TestClient(app) as c:
yield c
app.dependency_overrides.clear()
def test_me(client):
assert client.get("/me").status_code == 200
Using with TestClient(app) as c: also runs the app’s lifespan/startup/shutdown events — important if we have @app.on_event("startup") handlers.
Testing WebSockets
TestClient has websocket_connect baked in:
def test_chat():
with client.websocket_connect("/ws/chat/manish") as ws:
ws.send_text("hello")
msg = ws.receive_text()
assert "hello" in msg
Common patterns
- Auth in tests — override
get_current_userto return a fake user. Don’t deal with real tokens in unit tests. - DB in tests — either override
get_dbwith an in-memory SQLite session, or use the real DB inside a transaction that gets rolled back. - External APIs — override the client dependency, or use
respxto intercepthttpxcalls. - Don’t share state between tests — clear
dependency_overridesin teardown.
Interview cheat sheet
TestClient(app)for 95% of cases. Sync API even for async routes.httpx.AsyncClient+ASGITransportwhen test code itself needs to be async.app.dependency_overrides[dep] = faketo swap anything — auth, DB, clients.- Use
with TestClient(app)to trigger lifespan/startup. - Keep tests fast by overriding external dependencies — don’t hit real DBs or APIs.