FastAPI Tutorial Part 15: Testing Your API
Write comprehensive tests for FastAPI applications. Learn pytest, TestClient, database testing, mocking, and test-driven development patterns.
Moshiour Rahman
Advertisement
Setup
pip install pytest pytest-asyncio httpx
Basic Testing with TestClient
FastAPI provides TestClient (based on httpx and Starlette), which is extremely fast because it doesn’t start a real network server. It calls your FastAPI application directly.
How TestClient Works

# tests/test_main.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
def test_create_item():
response = client.post(
"/items",
json={"name": "Test Item", "price": 9.99}
)
assert response.status_code == 201
assert response.json()["name"] == "Test Item"
def test_get_item_not_found():
response = client.get("/items/999")
assert response.status_code == 404
Async Testing
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
@pytest.mark.asyncio
async def test_async_endpoint():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/async-endpoint")
assert response.status_code == 200
Database Testing
# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db
from app.main import app
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
TestingSessionLocal = sessionmaker(bind=engine)
@pytest.fixture
def db_session():
Base.metadata.create_all(bind=engine)
session = TestingSessionLocal()
try:
yield session
finally:
session.close()
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def client(db_session):
def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app)
app.dependency_overrides.clear()
Testing Authentication
@pytest.fixture
def auth_headers(client):
# Register user
client.post("/auth/register", json={
"email": "test@test.com",
"username": "testuser",
"password": "testpass123"
})
# Login
response = client.post("/auth/login", data={
"username": "testuser",
"password": "testpass123"
})
token = response.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
def test_protected_endpoint(client, auth_headers):
response = client.get("/users/me", headers=auth_headers)
assert response.status_code == 200
assert response.json()["username"] == "testuser"
def test_unauthorized_access(client):
response = client.get("/users/me")
assert response.status_code == 401
Mocking
from unittest.mock import patch, MagicMock
def test_external_api_call(client):
with patch("app.services.external_api.fetch_data") as mock:
mock.return_value = {"data": "mocked"}
response = client.get("/external-data")
assert response.json()["data"] == "mocked"
Complete Test Suite
# tests/test_items.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
class TestItems:
@pytest.fixture(autouse=True)
def setup(self, client):
self.client = client
def test_create_item(self):
response = self.client.post("/items", json={
"name": "Widget",
"price": 29.99,
"category": "electronics"
})
assert response.status_code == 201
data = response.json()
assert data["name"] == "Widget"
assert "id" in data
def test_list_items(self):
# Create items
for i in range(3):
self.client.post("/items", json={
"name": f"Item {i}",
"price": 10.0 + i,
"category": "test"
})
response = self.client.get("/items")
assert response.status_code == 200
assert len(response.json()["items"]) >= 3
def test_filter_items(self):
response = self.client.get("/items?min_price=20&max_price=50")
assert response.status_code == 200
def test_validation_error(self):
response = self.client.post("/items", json={
"name": "", # Invalid
"price": -10 # Invalid
})
assert response.status_code == 422
Running Tests
# Run all tests
pytest
# With coverage
pytest --cov=app --cov-report=html
# Specific test file
pytest tests/test_items.py -v
# Run specific test
pytest tests/test_items.py::TestItems::test_create_item -v
Summary
| Tool | Purpose |
|---|---|
TestClient | Sync testing |
AsyncClient | Async testing |
pytest.fixture | Setup/teardown |
mock.patch | Mocking external calls |
Next Steps
In Part 16, we’ll explore Docker and Deployment - containerizing and deploying FastAPI applications.
Series Navigation:
- Part 1-14: Previous parts
- Part 15: Testing (You are here)
- Part 16: Docker & Deployment
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
FastAPI Tutorial Part 20: Building a Production-Ready API
Build a complete production-ready FastAPI application. Combine all concepts into a real-world e-commerce API with authentication, database, and deployment.
PythonFastAPI Tutorial Part 19: OpenAPI and API Documentation
Create professional API documentation with FastAPI. Learn OpenAPI customization, Swagger UI, ReDoc, and documentation best practices.
PythonFastAPI Tutorial Part 16: Docker and Deployment
Deploy FastAPI applications to production. Learn Docker containerization, Docker Compose, cloud deployment, and production best practices.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.