FastAPI’s app.dependency_overrides dictionary replaces any dependency with a different callable for the duration of testing. This is the mechanism that makes FastAPI applications highly testable — instead of hitting a real database, authentication system, or external service, tests swap in lightweight fakes that control all inputs. The pattern is: define a factory function that creates the override, call app.dependency_overrides[original_dep] = override_factory, run the test, then clear the override. Combined with pytest’s conftest.py for fixtures, this produces a clean, reproducible test environment.
Basic Dependency Override Pattern
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine, event
from sqlalchemy.orm import Session, sessionmaker
from app.main import app
from app.dependencies import get_db, get_current_user
from app.database import Base
from app.models.user import User
# ── Test database — SQLite in-memory for speed ────────────────────────────────
TEST_DATABASE_URL = "sqlite:///./test.db" # or "sqlite:///:memory:"
test_engine = create_engine(
TEST_DATABASE_URL,
connect_args={"check_same_thread": False}
)
TestSessionLocal = sessionmaker(bind=test_engine)
@pytest.fixture(scope="session", autouse=True)
def create_test_tables():
Base.metadata.create_all(test_engine)
yield
Base.metadata.drop_all(test_engine)
@pytest.fixture
def db_session():
"""
Yields a session that rolls back all changes after each test.
Ensures test isolation without recreating the schema.
"""
connection = test_engine.connect()
transaction = connection.begin()
session = Session(bind=connection)
yield session
session.close()
transaction.rollback()
connection.close()
@pytest.fixture
def test_user(db_session) -> User:
"""Create a test user in the test database."""
user = User(
email = "test@example.com",
name = "Test User",
password_hash = "hashed_password",
role = "user",
)
db_session.add(user)
db_session.flush()
return user
scope="function" default means a fresh rolled-back session for every test function, guaranteeing isolation. Use scope="module" only if you are certain tests do not interfere with each other.test_db PostgreSQL database and set TEST_DATABASE_URL to point to it in your CI environment.app.dependency_overrides after each test that sets them, or use a pytest fixture with yield to guarantee cleanup. If you forget to clear overrides, a subsequent test that expects the real dependency will still get the fake, producing false positives or confusing failures. The safest pattern is to set overrides in a fixture with yield and clear them in the fixture’s teardown.Complete Test Client Setup
# tests/conftest.py (continued)
from fastapi.testclient import TestClient
@pytest.fixture
def client(db_session, test_user) -> TestClient:
"""
Returns a TestClient with get_db and get_current_user overridden.
"""
def override_get_db():
yield db_session # use the test session (auto-rolls-back)
def override_get_current_user():
return test_user # always return the test user
app.dependency_overrides[get_db] = override_get_db
app.dependency_overrides[get_current_user] = override_get_current_user
yield TestClient(app)
app.dependency_overrides.clear() # crucial: reset after each test
@pytest.fixture
def anonymous_client(db_session) -> TestClient:
"""
TestClient without authentication — for testing public endpoints
and verifying 401 responses on protected endpoints.
"""
def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app)
app.dependency_overrides.clear()
Writing Integration Tests
# tests/test_posts.py
import pytest
from fastapi.testclient import TestClient
from app.models.post import Post
class TestPostEndpoints:
def test_list_posts_returns_empty_for_fresh_db(self, client):
response = client.get("/posts/")
assert response.status_code == 200
data = response.json()
assert data["total"] == 0
assert data["items"] == []
def test_create_post_success(self, client, db_session, test_user):
response = client.post("/posts/", json={
"title": "Test Post",
"body": "This is the post body content.",
"slug": "test-post",
})
assert response.status_code == 201
data = response.json()
assert data["title"] == "Test Post"
assert data["author_id"] == test_user.id
# Verify in database
post = db_session.get(Post, data["id"])
assert post is not None
assert post.title == "Test Post"
def test_create_post_unauthenticated(self, anonymous_client):
response = anonymous_client.post("/posts/", json={
"title": "Test", "body": "Body content", "slug": "test"
})
assert response.status_code == 401
def test_get_nonexistent_post_returns_404(self, client):
response = client.get("/posts/999999")
assert response.status_code == 404
def test_update_post_by_owner(self, client, db_session, test_user):
# Create a post first
post = Post(title="Old", body="Old body", slug="old",
author_id=test_user.id, status="published")
db_session.add(post)
db_session.flush()
response = client.patch(f"/posts/{post.id}", json={"title": "New Title"})
assert response.status_code == 200
assert response.json()["title"] == "New Title"
def test_delete_post_by_non_owner_returns_403(self, client, db_session):
other_user_id = 999 # different from test_user.id
post = Post(title="Other's Post", body="Body", slug="others-post",
author_id=other_user_id, status="published")
db_session.add(post)
db_session.flush()
response = client.delete(f"/posts/{post.id}")
assert response.status_code == 403
Overriding for Admin Tests
@pytest.fixture
def admin_client(db_session) -> TestClient:
admin_user = User(
email="admin@example.com",
name="Admin",
password_hash="hash",
role="admin",
)
db_session.add(admin_user)
db_session.flush()
def override_get_db():
yield db_session
def override_get_current_user():
return admin_user
app.dependency_overrides[get_db] = override_get_db
app.dependency_overrides[get_current_user] = override_get_current_user
yield TestClient(app)
app.dependency_overrides.clear()
# Test admin-only endpoint
def test_hard_delete_requires_admin(client, admin_client, db_session, test_user):
post = Post(title="Delete Me", body="Body", slug="delete-me",
author_id=test_user.id, status="published")
db_session.add(post)
db_session.flush()
# Regular user cannot hard-delete
r1 = client.delete(f"/posts/{post.id}/hard-delete")
assert r1.status_code == 403
# Admin can hard-delete
r2 = admin_client.delete(f"/posts/{post.id}/hard-delete")
assert r2.status_code == 204
Common Mistakes
Mistake 1 — Not clearing dependency_overrides after tests
❌ Wrong — override leaks into next test:
def test_something():
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
...
# No cleanup! Next test gets the fake db too
✅ Correct — clear in fixture teardown:
@pytest.fixture
def client():
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app)
app.dependency_overrides.clear() # ✓ always runs, even on test failure
Mistake 2 — Using SQLite for PostgreSQL-specific features
❌ Wrong — tsvector query fails silently in SQLite:
TEST_DATABASE_URL = "sqlite:///./test.db" # no JSONB, tsvector, etc!
✅ Correct — use PostgreSQL for the test database when the app uses PG-specific features.
Mistake 3 — Not rolling back between tests (test pollution)
❌ Wrong — test data from test A affects test B:
def test_a():
Post(title="Leftover").save() # stays in DB
def test_b():
count = Post.count() # sees leftover from test_a!
✅ Correct — roll back the test transaction after each test (as in the fixture above).
Quick Reference
| Task | Code |
|---|---|
| Override dependency | app.dependency_overrides[dep] = override |
| Clear all overrides | app.dependency_overrides.clear() |
| Test client | TestClient(app) (from fastapi.testclient) |
| Rollback per test | Wrap session in a transaction, rollback in fixture teardown |
| Override auth | app.dependency_overrides[get_current_user] = lambda: test_user |
| Anonymous client | Override get_db only, not get_current_user |