DEV Community

MSakai
MSakai

Posted on

Test your FastAPI routes without touching the database

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")
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode
# 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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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