Testing FastAPI Apps
TestClient, and a complete endpoint test using a mocked dependency via dependency_overrides.
TestClient
FastAPI's TestClient (built on httpx) lets you call your app's endpoints directly in a test, with no running server or network involved — requests go straight through the same ASGI app your real deployment runs.
pip install httpx pytest
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
# test_main.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_read_item():
response = client.get("/items/42?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 42, "q": "test"}
def test_read_item_invalid_id_returns_422():
response = client.get("/items/not-an-int")
assert response.status_code == 422
pytest
TestClient(app) wraps your actual FastAPI instance — every test request goes through the real routing, dependency resolution, and Pydantic validation exactly as a live request would, just without opening a real TCP socket.
A complete test with a mocked dependency
Testing an endpoint that depends on a database session or an external service is where Depends() pays off directly — FastAPI lets you swap out any dependency for a fake one during tests via app.dependency_overrides, with zero changes to the endpoint code itself.
# main.py
from fastapi import Depends, FastAPI
app = FastAPI()
def get_db():
db = RealDatabaseSession()
try:
yield db
finally:
db.close()
@app.get("/users/{user_id}")
def read_user(user_id: int, db=Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if user is None:
return {"error": "not found"}
return {"id": user.id, "name": user.name}
# test_main.py
from fastapi.testclient import TestClient
from main import app, get_db
class FakeUser:
def __init__(self, id, name):
self.id = id
self.name = name
class FakeDB:
def __init__(self, users):
self._users = {u.id: u for u in users}
def query(self, model):
return self # a tiny stand-in supporting only .filter().first()
def filter(self, *args, **kwargs):
return self
def first(self):
return next(iter(self._users.values()), None)
def override_get_db():
yield FakeDB([FakeUser(id=1, name="Ada Lovelace")])
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
def test_read_existing_user():
response = client.get("/users/1")
assert response.status_code == 200
assert response.json() == {"id": 1, "name": "Ada Lovelace"}
def test_read_missing_user():
app.dependency_overrides[get_db] = lambda: iter([FakeDB([])])
response = client.get("/users/999")
assert response.json() == {"error": "not found"}
app.dependency_overrides is a dict keyed by the original dependency function — assigning app.dependency_overrides[get_db] = override_get_db tells FastAPI to call override_get_db instead of get_db for the lifetime of that override, for every request the test client makes. This is the standard way to substitute a real database (or a real external HTTP call) with a fast, in-memory fake for tests, without touching the endpoint's own code at all — the endpoint still just writes db=Depends(get_db).
Common mistakes
- Testing against a real database or a real third-party API instead of overriding the dependency — tests become slow, flaky, and dependent on network/service availability instead of pure and repeatable.
- Forgetting to reset
app.dependency_overridesbetween tests (or between test modules) — an override left in place from one test can silently affect a later test that expected the real dependency. - Asserting only on
status_codeand skippingresponse.json()— a200with the wrong body still passes a status-only test while genuinely being broken.
Interview questions
Q: How does TestClient let you test a FastAPI app without running a real server?
It wraps your ASGI application directly and simulates HTTP requests against it in-process, running the exact same routing, dependency resolution, and validation code a real deployed request would hit — no TCP socket, port, or running Uvicorn process is involved. This makes tests fast and self-contained while still exercising real framework behavior, not just your bare Python functions.
Q: How do you test an endpoint that depends on a database, without hitting a real database in your test suite?
Override the dependency function via app.dependency_overrides[get_db] = fake_get_db, where fake_get_db yields an in-memory fake or a test-database session instead of a real production connection. Because the endpoint only ever asks for whatever Depends(get_db) resolves to, it runs unmodified against the fake — the substitution happens entirely at the dependency-injection layer, not inside the endpoint's own code.