Skip to main content

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

PitfallCause / SymptomFix
Overrides leak between testsapp.dependency_overrides.clear() not calledUse autouse fixture or always call clear in teardown
Mock returns wrong typeMock not matching actual return typeUse spec=ActualClass in MagicMock
Override not appliedOverriding the wrong function referenceImport from the exact module where it's used, not where it's defined
Async mock not awaitedSync MagicMock used for async dependencyUse AsyncMock for async dependencies
Test passes locally, fails in CICI doesn't have override appliedCheck 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

What's Next