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
# 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 14: File Uploads and Storage
Handle file uploads in FastAPI. Learn form data, file validation, cloud storage integration with S3, and serving static files.
PythonFastAPI Tutorial Part 11: Background Tasks and Celery
Handle long-running operations in FastAPI. Learn built-in BackgroundTasks, Celery integration, task queues, and async processing patterns.
PythonFastAPI Tutorial Part 7: CRUD Operations - Build a Complete REST API
Build production-ready CRUD APIs with FastAPI. Learn RESTful patterns, pagination, filtering, bulk operations, and best practices for real-world applications.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.