The usual first attempt at testing a FastAPI route reaches for patch:
with patch("myapp.routes.get_db") as mock_db:
response = client.get("/users/1")
It doesn't work, and the reason is instructive. FastAPI resolved Depends(get_db) when the route was registered, at import time. The function object it holds is not the one your patch replaced.
The intended mechanism
FastAPI keeps a mapping on the app for exactly this:
# myapp/deps.py
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from myapp.main import app
from myapp.deps import get_db
@pytest.fixture
def client(test_session):
def override_get_db():
yield test_session
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app)
app.dependency_overrides.clear()
The key is the function object itself, not a string path. No import-time subtlety, no patch target to get wrong.
That clear() in the teardown matters. app is a module-level singleton shared by every test, so a leftover override silently applies to the rest of the suite.
Where this pays off most: auth
Testing an authenticated route usually means minting a real JWT, which means your route tests now depend on your token library, your clock, and your secret configuration.
def override_current_user():
return User(id=1, email="test@example.com", is_admin=False)
app.dependency_overrides[get_current_user] = override_current_user
One line, and every protected route is testable. Test the token logic once, directly, in its own file — where it belongs.
Overriding per test, not per suite
Make the override a parameter and you can vary it:
@pytest.fixture
def client_as():
def _make(user: User):
app.dependency_overrides[get_current_user] = lambda: user
return TestClient(app)
yield _make
app.dependency_overrides.clear()
def test_admin_can_delete(client_as):
r = client_as(User(id=1, is_admin=True)).delete("/posts/1")
assert r.status_code == 204
def test_regular_user_cannot(client_as):
r = client_as(User(id=2, is_admin=False)).delete("/posts/1")
assert r.status_code == 403
Two authorisation cases, no tokens, no database.
What to still use a real database for
Overrides are the right tool for routes. They are the wrong tool for anything where the database itself is the thing under test — constraints, cascades, migrations, transaction boundaries.
For those, an in-process SQLite session is usually enough:
@pytest.fixture
def test_session():
engine = create_engine("sqlite://", connect_args={"check_same_thread": False})
Base.metadata.create_all(engine)
with Session(engine) as s:
yield s
...with the caveat that SQLite will not reproduce Postgres behaviour for JSON columns, array types, or ON CONFLICT specifics. When those matter, reach for a real Postgres in a container and accept the slower feedback loop for that one file.
The distinction worth keeping
Route tests answer "does this endpoint wire up correctly?" Database tests answer "does this data model hold?" Overrides make the first kind fast. Don't use them to avoid writing the second kind.
These posts come out of material I build for my Udemy courses — 25 of them now, mostly drill-based, across Go, Python, TypeScript, testing and Three.js. If this was useful, the full list is at udemy-c1f90.web.app. The links on that page carry a coupon I refresh each month, which usually lands around half the list price.
Top comments (0)