Skip to main content

FastAPI Dependency Testing

In FastAPI applications, dependencies play a crucial role in organizing code, handling authentication, managing database connections, and more. Testing these dependencies properly ensures your application remains robust and reliable. This guide will walk you through effective strategies for testing FastAPI dependencies.

Introduction to Testing Dependencies

When building FastAPI applications with dependency injection, testing becomes both important and potentially complex. Dependencies might connect to databases, external APIs, or perform other operations that shouldn't run during tests. Proper testing strategy helps you:

  • Isolate the code being tested
  • Avoid making real external calls
  • Test error scenarios without causing actual errors
  • Make tests run faster and more reliably

Basic Concepts of Dependency Testing

Types of Dependencies to Test

In FastAPI, you'll typically need to test these types of dependencies:

  1. Simple dependencies - Functions that return values
  2. Class dependencies - Classes with __call__ methods
  3. Nested dependencies - Dependencies that depend on other dependencies
  4. Dependencies with yields - For setup/teardown operations

Testing Simple Dependencies

Let's start with testing a simple dependency function:

python
# app/dependencies.py
async def get_current_user(token: str = Depends(oauth2_scheme)):
user = await verify_token(token)
if not user:
raise HTTPException(status_code=401, detail="Invalid authentication credentials")
return user

To test this dependency, you'll need to mock the verify_token function and test both success and failure cases:

python
# tests/test_dependencies.py
from unittest.mock import patch, AsyncMock
import pytest
from fastapi import HTTPException
from app.dependencies import get_current_user

@pytest.mark.asyncio
async def test_get_current_user_valid():
# Mock the verify_token function to return a user
with patch("app.dependencies.verify_token", new_callable=AsyncMock) as mock_verify:
mock_verify.return_value = {"id": 1, "username": "testuser"}

# Call the dependency function
user = await get_current_user("valid_token")

# Assert the function returns the expected user
assert user["id"] == 1
assert user["username"] == "testuser"

# Assert verify_token was called with the correct token
mock_verify.assert_called_once_with("valid_token")

@pytest.mark.asyncio
async def test_get_current_user_invalid():
# Mock the verify_token function to return None (invalid token)
with patch("app.dependencies.verify_token", new_callable=AsyncMock) as mock_verify:
mock_verify.return_value = None

# Test that the function raises an HTTPException for invalid token
with pytest.raises(HTTPException) as excinfo:
await get_current_user("invalid_token")

# Verify the exception details
assert excinfo.value.status_code == 401
assert "Invalid authentication credentials" in excinfo.value.detail

Testing Class-based Dependencies

When testing class-based dependencies, you'll need to create instances of the class and test their __call__ method:

python
# app/dependencies.py
class DatabaseDependency:
def __init__(self):
self.connection = None

async def __call__(self):
self.connection = await create_db_connection()
try:
yield self.connection
finally:
await self.connection.close()

db_dependency = DatabaseDependency()

The test might look like:

python
# tests/test_dependencies.py
@pytest.mark.asyncio
async def test_db_dependency():
# Create mocks for the connection
mock_connection = AsyncMock()

# Mock the create_db_connection function
with patch("app.dependencies.create_db_connection", new_callable=AsyncMock) as mock_create:
mock_create.return_value = mock_connection

# Create an instance of the dependency
db_dep = DatabaseDependency()

# Use it in a context manager to test the yield behavior
async for conn in db_dep():
# Test that the connection is the mock connection
assert conn is mock_connection
# Do some operations with the connection
conn.execute.return_value = ["result1", "result2"]
result = await conn.execute("SELECT * FROM table")
assert result == ["result1", "result2"]

# Test that connection.close was called
mock_connection.close.assert_called_once()

Testing Nested Dependencies

Testing nested dependencies requires careful mocking of each layer. Let's consider this example:

python
# app/dependencies.py
async def get_db_connection():
connection = await create_db_connection()
try:
yield connection
finally:
await connection.close()

async def get_user_repository(db=Depends(get_db_connection)):
return UserRepository(db)

async def get_current_user(
token: str = Depends(oauth2_scheme),
user_repo=Depends(get_user_repository)
):
user_id = decode_token(token)
user = await user_repo.get_by_id(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user

You can test it like this:

python
@pytest.mark.asyncio
async def test_get_current_user_nested():
# Create a mock user repository
mock_user_repo = AsyncMock()
mock_user_repo.get_by_id.return_value = {"id": 1, "username": "testuser"}

# Mock the decode_token function
with patch("app.dependencies.decode_token") as mock_decode:
mock_decode.return_value = 1 # User ID

# Test the get_current_user function with a mocked user_repo
user = await get_current_user("fake_token", mock_user_repo)

# Assertions
assert user["id"] == 1
assert user["username"] == "testuser"
mock_user_repo.get_by_id.assert_called_once_with(1)

Integration Testing with TestClient

While unit testing individual dependencies is important, you should also test them integrated into your endpoints:

python
# tests/test_integration.py
from fastapi.testclient import TestClient
from unittest.mock import patch, AsyncMock
from app.main import app

client = TestClient(app)

def test_protected_endpoint():
# Mock the dependency directly in the app
with patch("app.main.get_current_user", return_value={"id": 1, "username": "testuser"}):
response = client.get("/protected-endpoint")
assert response.status_code == 200
assert "message" in response.json()

def test_protected_endpoint_unauthorized():
# Mock the dependency to raise an HTTPException
with patch("app.main.get_current_user", side_effect=HTTPException(status_code=401)):
response = client.get("/protected-endpoint")
assert response.status_code == 401

Using Override_dependency for Testing

FastAPI provides a powerful way to override dependencies for testing using app.dependency_overrides:

python
# tests/test_override.py
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

async def override_get_current_user():
# This will be used instead of the real dependency
return {"id": 1, "username": "testuser"}

def test_with_override():
# Set up dependency override
app.dependency_overrides[get_current_user] = override_get_current_user

# Make the request
response = client.get("/protected-endpoint")

# Assertions
assert response.status_code == 200

# Clean up after test
app.dependency_overrides = {}

Testing Dependencies with Yields

For dependencies that yield values (like database connections), you need to test both the setup and teardown logic:

python
@pytest.mark.asyncio
async def test_dependency_with_yield():
mock_conn = AsyncMock()

with patch("app.dependencies.create_connection", return_value=mock_conn):
# Use the dependency in an async for loop to capture the yielded value
dep_gen = get_db_connection()
connection = await dep_gen.__anext__()

# Test the yielded connection
assert connection == mock_conn

# Test cleanup by calling __anext__ until it raises StopAsyncIteration
try:
await dep_gen.__anext__()
assert False, "Expected StopAsyncIteration"
except StopAsyncIteration:
# Make sure the connection was closed
mock_conn.close.assert_called_once()

Creating Test Fixtures for Dependencies

Using pytest fixtures can make dependency testing more organized:

python
# tests/conftest.py
import pytest
from unittest.mock import AsyncMock, patch

@pytest.fixture
def mock_db_connection():
"""Fixture for database connection dependency"""
mock_conn = AsyncMock()
mock_conn.execute.return_value = {"result": "data"}
return mock_conn

@pytest.fixture
def override_db_dependency(mock_db_connection):
"""Fixture to override database dependency in the app"""
async def override():
yield mock_db_connection

from app.main import app
from app.dependencies import get_db_connection

# Set the override
app.dependency_overrides[get_db_connection] = override

yield mock_db_connection

# Clean up after test
app.dependency_overrides = {}

Then in your tests:

python
# tests/test_endpoints.py
def test_user_endpoint(client, override_db_dependency):
# The db dependency is already overridden
response = client.get("/users/1")
assert response.status_code == 200
# Check that the mock was used
override_db_dependency.execute.assert_called_once()

Real-world Example: Testing Authentication Dependencies

Let's look at a complete example for a common use case - testing JWT authentication:

python
# app/auth.py
from fastapi import Depends, HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from datetime import datetime, timedelta

security = HTTPBearer()
SECRET_KEY = "your_secret_key"
ALGORITHM = "HS256"

def create_access_token(data: dict, expires_delta: timedelta = timedelta(minutes=15)):
to_encode = data.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt

async def get_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
try:
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise HTTPException(status_code=401, detail="Invalid authentication credentials")
except jwt.PyJWTError:
raise HTTPException(status_code=401, detail="Invalid authentication token")

# Here you would typically load the user from a database
user = {"username": username, "is_active": True}
if not user["is_active"]:
raise HTTPException(status_code=400, detail="Inactive user")
return user

Here's how to test it:

python
# tests/test_auth.py
import pytest
from unittest.mock import patch
import jwt
from datetime import datetime, timedelta
from fastapi import HTTPException
from fastapi.security import HTTPAuthorizationCredentials
from app.auth import get_current_user, create_access_token, SECRET_KEY, ALGORITHM

# Test creating tokens
def test_create_access_token():
data = {"sub": "testuser"}
token = create_access_token(data)
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

# Check token content
assert payload["sub"] == "testuser"
assert "exp" in payload

# Check expiration time is approximately 15 minutes in the future
exp_time = datetime.fromtimestamp(payload["exp"])
now_plus_15 = datetime.utcnow() + timedelta(minutes=15)
assert abs((exp_time - now_plus_15).total_seconds()) < 10 # Within 10 seconds

# Test authentication succeeding
@pytest.mark.asyncio
async def test_get_current_user_valid():
# Create a valid token
token = create_access_token({"sub": "testuser"})

# Create credentials object
credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)

# Call the dependency function
user = await get_current_user(credentials)

# Check the returned user
assert user["username"] == "testuser"
assert user["is_active"] == True

# Test authentication failing with invalid token
@pytest.mark.asyncio
async def test_get_current_user_invalid_token():
# Create invalid credentials
credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials="invalid-token")

# Test exception is raised
with pytest.raises(HTTPException) as excinfo:
await get_current_user(credentials)

# Check exception details
assert excinfo.value.status_code == 401
assert "Invalid authentication token" in excinfo.value.detail

# Test authentication failing with token missing 'sub' claim
@pytest.mark.asyncio
async def test_get_current_user_missing_sub():
# Create a token with no 'sub' claim
token = create_access_token({"data": "no-username"})

# Create credentials object
credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)

# Test exception is raised
with pytest.raises(HTTPException) as excinfo:
await get_current_user(credentials)

# Check exception details
assert excinfo.value.status_code == 401
assert "Invalid authentication credentials" in excinfo.value.detail

Summary

Testing dependencies in FastAPI applications requires:

  1. Isolation - Mock external services and dependencies
  2. Coverage - Test both success and failure conditions
  3. Structure - Use fixtures to organize tests
  4. Integration - Test how dependencies work within endpoints

Effective testing strategies include:

  • Using unittest.mock to patch functions and methods
  • Creating pytest fixtures for common test scenarios
  • Using app.dependency_overrides to replace dependencies
  • Testing both setup and teardown for dependencies with yields

By thoroughly testing your dependencies, you ensure your FastAPI application remains robust, secure, and maintainable as it grows in complexity.

Additional Resources

Exercises

  1. Write tests for a dependency that accesses a database connection
  2. Create a test suite for a multi-level nested dependency system
  3. Test a caching dependency that stores results in memory
  4. Write tests for a rate-limiting dependency
  5. Create a comprehensive test suite for an authentication system with roles and permissions


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)