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:
- Simple dependencies - Functions that return values
- Class dependencies - Classes with
__call__
methods - Nested dependencies - Dependencies that depend on other dependencies
- Dependencies with yields - For setup/teardown operations
Testing Simple Dependencies
Let's start with testing a simple dependency function:
# 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:
# 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:
# 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:
# 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:
# 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:
@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:
# 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
:
# 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:
@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:
# 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:
# 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:
# 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:
# 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:
- Isolation - Mock external services and dependencies
- Coverage - Test both success and failure conditions
- Structure - Use fixtures to organize tests
- 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
- FastAPI Official Testing Documentation
- pytest Documentation
- unittest.mock Documentation
- Dependency Injection in FastAPI
Exercises
- Write tests for a dependency that accesses a database connection
- Create a test suite for a multi-level nested dependency system
- Test a caching dependency that stores results in memory
- Write tests for a rate-limiting dependency
- 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! :)