Mocking Dependencies
FastAPI's dependency_overrides is the cleanest way to mock any dependency in tests — no monkey-patching, no complex mocking frameworks. You replace a dependency function with a test double for the duration of a test.
Learning Focus
By the end of this lesson you can: override DB sessions, authentication, external API clients, and feature flags in test code — and always clean up overrides after each test.
Basic Override Pattern
tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.db.session import get_db
from app.dependencies.auth import get_current_user
@pytest.fixture
def client_no_auth():
"""Client with no authentication — useful for testing public routes."""
with TestClient(app) as c:
yield c
@pytest.fixture
def client_as_admin():
"""Client authenticated as an admin user."""
async def fake_admin():
return type("User", (), {"id": 1, "username": "admin", "role": "admin", "is_active": True})()
app.dependency_overrides[get_current_user] = fake_admin
with TestClient(app) as c:
yield c
app.dependency_overrides.clear() # Always clean up
@pytest.fixture
def client_as_user():
"""Client authenticated as a regular user."""
async def fake_user():
return type("User", (), {"id": 2, "username": "alice", "role": "user", "is_active": True})()
app.dependency_overrides[get_current_user] = fake_user
with TestClient(app) as c:
yield c
app.dependency_overrides.clear()
Mocking the Database
tests/conftest.py
from unittest.mock import AsyncMock, MagicMock
@pytest.fixture
def mock_db():
"""A mock SQLAlchemy async session."""
session = AsyncMock()
session.execute = AsyncMock()
session.add = MagicMock()
session.flush = AsyncMock()
session.commit = AsyncMock()
session.rollback = AsyncMock()
return session
@pytest.fixture
def client_with_mock_db(mock_db):
async def override_db():
yield mock_db
app.dependency_overrides[get_db] = override_db
with TestClient(app) as c:
yield c, mock_db
app.dependency_overrides.clear()
tests/test_users.py
from unittest.mock import AsyncMock
def test_get_user_calls_db(client_with_mock_db):
client, mock_db = client_with_mock_db
# Set up mock return value
mock_user = MagicMock(id=1, username="alice", email="alice@example.com")
mock_db.execute.return_value.scalar_one_or_none = MagicMock(return_value=mock_user)
resp = client.get("/users/1")
assert resp.status_code == 200
mock_db.execute.assert_called_once()
Mocking External Services
tests/conftest.py
import pytest
from unittest.mock import patch, AsyncMock
@pytest.fixture
def mock_email_service():
with patch("app.services.email.send_email", new_callable=AsyncMock) as mock:
yield mock
def test_signup_sends_email(client, mock_email_service):
resp = client.post("/signups/", json={"username": "alice", "email": "a@example.com"})
assert resp.status_code == 201
mock_email_service.assert_called_once_with(to="a@example.com", subject="Welcome!")
Mocking Feature Flags
tests/test_features.py
from app.dependencies.features import feature_enabled
@pytest.fixture
def client_with_beta():
async def beta_enabled():
return # Does nothing — feature is "enabled"
app.dependency_overrides[feature_enabled("BETA_ENABLED")] = beta_enabled
with TestClient(app) as c:
yield c
app.dependency_overrides.clear()
Override Cleanup Patterns
Always clear overrides. Use pytest's autouse fixture for global cleanup:
tests/conftest.py
import pytest
from app.main import app
@pytest.fixture(autouse=True)
def clear_dependency_overrides():
yield
app.dependency_overrides.clear()
With autouse=True, this runs after every test in the module — even if the test fails.
Common Pitfalls
| Pitfall | Cause / Symptom | Fix |
|---|---|---|
| Overrides leak between tests | app.dependency_overrides.clear() not called | Use autouse fixture or always call clear in teardown |
| Mock returns wrong type | Mock not matching actual return type | Use spec=ActualClass in MagicMock |
| Override not applied | Overriding the wrong function reference | Import from the exact module where it's used, not where it's defined |
| Async mock not awaited | Sync MagicMock used for async dependency | Use AsyncMock for async dependencies |
| Test passes locally, fails in CI | CI doesn't have override applied | Check test isolation and autouse fixture scope |
Hands-On Practice
tests/test_role_access.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.dependencies.auth import get_current_user
def make_user(role: str):
async def _user():
return type("U", (), {"id": 1, "role": role, "is_active": True})()
return _user
@pytest.fixture(autouse=True)
def clean_overrides():
yield
app.dependency_overrides.clear()
@pytest.fixture
def as_admin():
app.dependency_overrides[get_current_user] = make_user("admin")
return TestClient(app)
@pytest.fixture
def as_user():
app.dependency_overrides[get_current_user] = make_user("user")
return TestClient(app)
def test_admin_can_access_dashboard(as_admin):
resp = as_admin.get("/admin/dashboard")
assert resp.status_code == 200
def test_user_cannot_access_dashboard(as_user):
resp = as_user.get("/admin/dashboard")
assert resp.status_code == 403
run-tests.sh
pytest tests/test_role_access.py -v