Skip to main content

Testing FastAPI Dependency Injection: A Comprehensive Guide

ยท 7 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

Testing code that uses Dependency Injection (DI) is crucial because it allows you to isolate the logic of your route handlers from the complexity of external services (like databases, security checks, or configuration). FastAPI makes this straightforward using the app.dependency_overrides dictionary.

By implementing overrides, you replace the original, complex dependency function with a simple mock function that returns predictable test data, ensuring your tests run fast and consistently.

1. The Core Principle: Overriding Dependenciesโ€‹

The primary tool for testing dependencies is the app.dependency_overrides dictionary. You map the original dependency function (the key) to your test mock function (the value). This override is active only for the duration of your test and must be cleaned up afterward.

Example Setup (Using pytest and httpx)โ€‹

import pytest
from fastapi.testclient import TestClient
from fastapi import FastAPI, Depends, Header, HTTPException
from typing import Annotated, Generator

# --- Application Setup ---

app = FastAPI()

# A. Dependency for Configuration (Simple)
def get_config():
return {"host": "prod.api.com", "limit": 50}
Config = Annotated[dict, Depends(get_config)]

# B. Dependency for Security (Raises Exception)
def check_admin_access(token: Annotated[str, Header(alias="X-Admin-Token")]):
if token != "ADMIN_SECRET":
raise HTTPException(status_code=401, detail="Access Denied")
return True
AdminAccess = Annotated[bool, Depends(check_admin_access)]

# C. Dependency with Yield (Resource Management)
def get_db_session() -> Generator[str, None, None]:
"""Simulates database session creation and cleanup."""
print("DB Session opened (Real)")
try:
yield "MOCK_REAL_SESSION"
finally:
print("DB Session closed (Real)")
DBSession = Annotated[str, Depends(get_db_session)]


# Sample Route using all dependencies
@app.get("/data")
def get_data(config: Config, is_admin: AdminAccess, db: DBSession):
return {
"host": config["host"],
"status": "ok",
"session": db
}

# --- Test Setup Fixture ---

@pytest.fixture(scope="module")
def client():
# 1. Create client
with TestClient(app) as c:
yield c
# 2. Cleanup is handled automatically by the context manager

2. Test Case 1: Testing Security Dependencies (Success & Failure)โ€‹

We test the route's behavior by mocking the security dependency to force a success or failure state.

# Override function to force success (Bypass security)
def override_check_admin_access():
return True

# Override function to force failure (Simulate invalid token)
def fail_check_admin_access():
raise HTTPException(status_code=401, detail="Mock Denied")

def test_security_success(client: TestClient):
# Map the original dependency (check_admin_access) to the mock override
app.dependency_overrides[check_admin_access] = override_check_admin_access

response = client.get("/data", headers={"X-Admin-Token": "ANY_FAKE_TOKEN"})
assert response.status_code == 200

# Crucial cleanup: Clear the override after the test
app.dependency_overrides = {}

def test_security_failure(client: TestClient):
# Map the original dependency to the mock failure function
app.dependency_overrides[check_admin_access] = fail_check_admin_access

response = client.get("/data")
assert response.status_code == 401
assert "Mock Denied" in response.json()["detail"]

app.dependency_overrides = {}

3. Test Case 2: Testing Dependencies with Simple Return Valuesโ€‹

This is the standard way to test configuration, service injection, or simple resource availability.

# Override function to provide specific test config
def override_get_config():
return {"host": "test.api.com", "limit": 100}

def test_config_injection(client: TestClient):
app.dependency_overrides[get_config] = override_get_config

response = client.get("/data", headers={"X-Admin-Token": "ADMIN_SECRET"})
assert response.status_code == 200
# Verify the mocked configuration value was used
assert response.json()["host"] == "test.api.com"

app.dependency_overrides = {}

4. Test Case 3: Testing Dependencies with yield (Resource Mocking)โ€‹

When testing yield dependencies (like database sessions), the override function must also be a generator or context manager that uses yield.

# Override function that is a context manager (uses yield)
def override_get_db_session() -> Generator[str, None, None]:
print("DB Session opened (Mock)")
try:
# Yield the mock resource name
yield "MOCK_TEST_SESSION"
finally:
# The cleanup code runs after the test assertion completes
print("DB Session closed (Mock)")

def test_yield_dependency(client: TestClient, capsys):
# Set the override
app.dependency_overrides[get_db_session] = override_get_db_session
app.dependency_overrides[check_admin_access] = override_check_admin_access # Ensure auth passes

# Execute the request
response = client.get("/data")
assert response.status_code == 200
# Verify the mocked session value was used in the response
assert response.json()["session"] == "MOCK_TEST_SESSION"

# Verify that the cleanup code ran (optional, but good for confidence)
captured = capsys.readouterr()
assert "DB Session opened (Mock)" in captured.out
assert "DB Session closed (Mock)" in captured.out

app.dependency_overrides = {}

5. Test Case 4: Testing Dependency Chains (Nested DI)โ€‹

If Dependency A relies on Dependency B, you only need to override the one you want to mock. You can override B to control the output seen by A, or override A to bypass B entirely.

Example: Controlling the Inner-Most Dependencyโ€‹

Assume the get_db_session dependency relies on a sub-dependency get_db_connection_url.

# --- Setup: New Dependency Chain ---
def get_db_connection_url():
return "postgres://real_url"

def get_db_session_nested(url: Annotated[str, Depends(get_db_connection_url)]):
print(f"Connecting to: {url}")
return "MOCKED_SESSION" # Simplification for this example

@app.get("/nested")
def nested_route(session: Annotated[str, Depends(get_db_session_nested)]):
return {"session_url": session}

# --- Test Case ---

def override_get_db_connection_url():
"""Mock the inner-most dependency (the URL)"""
return "MOCKED_URL"

def test_nested_dependency_override(client: TestClient, capsys):
# Override only the deepest dependency (get_db_connection_url)
app.dependency_overrides[get_db_connection_url] = override_get_db_connection_url

response = client.get("/nested")
assert response.status_code == 200

# Verify that the outer dependency (get_db_session_nested) ran
# and used the mocked URL from the inner dependency.
captured = capsys.readouterr()
assert "Connecting to: MOCKED_URL" in captured.out

app.dependency_overrides = {}

Sources and Further Readingโ€‹

  1. FastAPI Documentation - Testing with Dependencies
  2. FastAPI Documentation - Testing with yield Dependencies
  3. Python pytest Documentation - Fixtures
  4. Python httpx Documentation - TestClient