Testing is non-negotiable for production-ready APIs. FastAPI's design makes testing straightforward, and the Python ecosystem provides powerful tools to ensure your API works flawlessly. This guide covers the essential testing toolkit every FastAPI developer needs.
Core Testing Framework: pytest
pytest is the de facto testing framework for Python and FastAPI applications.
Installation
pip install pytest pytest-asyncio pytest-covBasic Test Structure
# tests/conftest.py
import pytest
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.database import Base, get_db
# Create test database engine
TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db"
engine = create_async_engine(TEST_DATABASE_URL)
TestingSessionLocal = sessionmaker(engine, class_=AsyncSession)
@pytest.fixture(scope="session")
def event_loop():
import asyncio
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture
async def db_session():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async with TestingSessionLocal() as session:
yield session
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
@pytest.fixture
async def client(db_session):
async def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test"
) as ac:
yield ac
app.dependency_overrides.clear()HTTP Testing: HTTPX and TestClient
HTTPX for Async Testing (Recommended)
# tests/test_users.py
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_create_user(client: AsyncClient):
response = await client.post(
"/api/v1/users",
json={
"email": "test@example.com",
"password": "securepassword123",
"name": "Test User"
}
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "test@example.com"
assert "id" in data
assert "password" not in data # Ensure password is not exposed
@pytest.mark.asyncio
async def test_get_user(client: AsyncClient, created_user):
response = await client.get(f"/api/v1/users/{created_user['id']}")
assert response.status_code == 200
assert response.json()["email"] == created_user["email"]
@pytest.mark.asyncio
async def test_create_user_invalid_email(client: AsyncClient):
response = await client.post(
"/api/v1/users",
json={"email": "invalid-email", "password": "test"}
)
assert response.status_code == 422
assert "email" in response.json()["detail"][0]["loc"]FastAPI TestClient (Sync Alternative)
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}Mocking and Patching
unittest.mock for Dependencies
from unittest.mock import AsyncMock, patch
import pytest
@pytest.mark.asyncio
async def test_payment_processing(client: AsyncClient):
# Mock external payment service
with patch("app.services.payment.stripe_client") as mock_stripe:
mock_stripe.create_charge = AsyncMock(return_value={
"id": "ch_123",
"status": "succeeded"
})
response = await client.post(
"/api/v1/payments",
json={"amount": 1000, "currency": "usd"}
)
assert response.status_code == 200
mock_stripe.create_charge.assert_called_once()pytest-mock for Cleaner Mocking
pip install pytest-mock@pytest.mark.asyncio
async def test_send_notification(client: AsyncClient, mocker):
mock_send = mocker.patch(
"app.services.email.send_email",
new_callable=AsyncMock
)
response = await client.post(
"/api/v1/notifications",
json={"user_id": 1, "message": "Hello"}
)
assert response.status_code == 200
mock_send.assert_called_once_with(
to="user@example.com",
subject=mocker.ANY,
body=mocker.ANY
)Respx for HTTP Mocking
pip install respximport respx
from httpx import Response
@pytest.mark.asyncio
async def test_external_api_call(client: AsyncClient):
with respx.mock:
respx.get("https://api.external.com/data").mock(
return_value=Response(200, json={"status": "ok"})
)
response = await client.get("/api/v1/fetch-external")
assert response.status_code == 200
assert response.json()["external_status"] == "ok"Database Testing
Factory Boy for Test Data
pip install factory-boy# tests/factories.py
import factory
from factory.alchemy import SQLAlchemyModelFactory
from app.models import User, Post
class UserFactory(SQLAlchemyModelFactory):
class Meta:
model = User
sqlalchemy_session_persistence = "commit"
email = factory.Sequence(lambda n: f"user{n}@example.com")
name = factory.Faker("name")
hashed_password = "hashedpassword123"
is_active = True
class PostFactory(SQLAlchemyModelFactory):
class Meta:
model = Post
title = factory.Faker("sentence")
content = factory.Faker("paragraph")
author = factory.SubFactory(UserFactory)# tests/test_posts.py
@pytest.mark.asyncio
async def test_list_user_posts(client: AsyncClient, db_session):
user = UserFactory(session=db_session)
PostFactory.create_batch(5, author=user, session=db_session)
response = await client.get(f"/api/v1/users/{user.id}/posts")
assert response.status_code == 200
assert len(response.json()) == 5Faker for Realistic Data
from faker import Faker
fake = Faker()
@pytest.mark.asyncio
async def test_user_profile_update(client: AsyncClient, auth_token):
new_data = {
"name": fake.name(),
"bio": fake.text(max_nb_chars=200),
"location": fake.city()
}
response = await client.patch(
"/api/v1/profile",
json=new_data,
headers={"Authorization": f"Bearer {auth_token}"}
)
assert response.status_code == 200
assert response.json()["name"] == new_data["name"]API Contract Testing
Schemathesis for Property-Based Testing
pip install schemathesis# tests/test_schema.py
import schemathesis
from app.main import app
schema = schemathesis.from_asgi("/openapi.json", app)
@schema.parametrize()
def test_api_contracts(case):
"""Tests all endpoints against their OpenAPI schema."""
response = case.call_asgi()
case.validate_response(response)Run from CLI:
schemathesis run http://localhost:8000/openapi.jsonHypothesis for Data Generation
pip install hypothesisfrom hypothesis import given, strategies as st
@given(
name=st.text(min_size=1, max_size=100),
age=st.integers(min_value=18, max_value=120)
)
@pytest.mark.asyncio
async def test_user_creation_properties(client: AsyncClient, name, age):
response = await client.post(
"/api/v1/users",
json={"name": name, "age": age, "email": f"{name}@test.com"}
)
if response.status_code == 201:
data = response.json()
assert data["name"] == name
assert data["age"] == agePerformance Testing
Locust for Load Testing
pip install locust# locustfile.py
from locust import HttpUser, task, between
class FastAPIUser(HttpUser):
wait_time = between(1, 3)
def on_start(self):
# Login and store token
response = self.client.post("/auth/login", json={
"email": "test@example.com",
"password": "password123"
})
self.token = response.json()["access_token"]
self.headers = {"Authorization": f"Bearer {self.token}"}
@task(3)
def list_items(self):
self.client.get("/api/v1/items", headers=self.headers)
@task(1)
def create_item(self):
self.client.post(
"/api/v1/items",
json={"name": "Test Item", "price": 9.99},
headers=self.headers
)
@task(2)
def get_item(self):
self.client.get("/api/v1/items/1", headers=self.headers)locust -f locustfile.py --host=http://localhost:8000pytest-benchmark for Micro-Benchmarks
pip install pytest-benchmarkdef test_serialization_performance(benchmark):
from app.schemas import UserResponse
from app.models import User
user = User(id=1, email="test@example.com", name="Test")
def serialize():
return UserResponse.model_validate(user)
result = benchmark(serialize)
assert result.email == "test@example.com"Code Coverage
pytest-cov
pip install pytest-cov# Run tests with coverage
pytest --cov=app --cov-report=html --cov-report=term-missing
# Set minimum coverage threshold
pytest --cov=app --cov-fail-under=80# pyproject.toml
[tool.coverage.run]
source = ["app"]
omit = ["app/tests/*", "app/migrations/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if TYPE_CHECKING:",
]End-to-End Testing
Playwright for Browser Testing
pip install playwright pytest-playwright
playwright install# tests/e2e/test_api_docs.py
import pytest
from playwright.async_api import Page
@pytest.mark.asyncio
async def test_swagger_ui_loads(page: Page):
await page.goto("http://localhost:8000/docs")
# Wait for Swagger UI to load
await page.wait_for_selector(".swagger-ui")
# Check title
title = await page.text_content(".title")
assert "FastAPI" in title
# Test endpoint expansion
await page.click("text=/api/v1/users")
await page.wait_for_selector(".responses-wrapper")Testing WebSockets
import pytest
from httpx_ws import aconnect_ws
@pytest.mark.asyncio
async def test_websocket_chat(client: AsyncClient):
async with aconnect_ws(
"http://test/ws/chat/room1",
client
) as ws:
# Send message
await ws.send_json({"message": "Hello"})
# Receive echo
response = await ws.receive_json()
assert response["message"] == "Hello"Complete Testing Configuration
pyproject.toml
[tool.pytest.ini_options]
minversion = "7.0"
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = [
"-v",
"--tb=short",
"--strict-markers",
"-ra",
]
markers = [
"slow: marks tests as slow",
"integration: marks tests as integration tests",
"e2e: marks tests as end-to-end tests",
]
[tool.coverage.run]
source = ["app"]
branch = true
[tool.coverage.report]
fail_under = 80
show_missing = trueTesting Best Practices Checklist
✅ Use async tests from day one — Avoid event loop issues later
✅ Isolate database state — Each test gets a clean slate
✅ Mock external services — Tests shouldn't depend on third parties
✅ Test edge cases — Empty inputs, large payloads, concurrent requests
✅ Measure coverage — Aim for 80%+ on critical paths
✅ Run tests in CI — Automate on every pull request
✅ Use factories — Don't duplicate test data setup
✅ Test error responses — Verify proper error handling
Conclusion
A comprehensive testing strategy is essential for building reliable FastAPI applications. Start with pytest and HTTPX for core testing, add mocking for external dependencies, integrate property-based testing for edge cases, and implement load testing before production deployments.
ForgeAPI includes a complete testing setup with fixtures, factories, and CI integration out of the box, so you can focus on writing tests instead of configuring tools.
Related Resources: