On this page
article
Testing FastAPI Applications
Test FastAPI apps with TestClient, pytest fixtures, mocking dependencies, and integration testing patterns.
FastAPI’s test utilities make it straightforward to write unit and integration tests without running a live server.
Setup
pip install pytest httpx pytest-asyncio
FastAPI includes TestClient based on Starlette — no server needed.
Basic Tests
# 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, FastAPI!"}
def test_create_item():
response = client.post("/items/", json={
"name": "Widget",
"price": 9.99,
})
assert response.status_code == 201
data = response.json()
assert data["name"] == "Widget"
assert "id" in data
def test_validation_error():
response = client.post("/items/", json={"name": ""})
assert response.status_code == 422
Run: pytest tests/ -v
Override Dependencies
Replace database or auth dependencies in tests:
from app.database import get_db
from app.main import app
def override_get_db():
db = TestSessionLocal()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
def test_with_test_db():
response = client.get("/items/")
assert response.status_code == 200
Reset overrides after tests:
import pytest
@pytest.fixture(autouse=True)
def reset_overrides():
yield
app.dependency_overrides.clear()
Fixtures for Test Data
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.database import Base, get_db
SQLALCHEMY_TEST_URL = "sqlite:///:memory:"
engine = create_engine(SQLALCHEMY_TEST_URL, connect_args={"check_same_thread": False})
TestSession = sessionmaker(bind=engine)
@pytest.fixture
def db_session():
Base.metadata.create_all(bind=engine)
session = TestSession()
yield session
session.close()
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def client(db_session):
def override_db():
yield db_session
app.dependency_overrides[get_db] = override_db
with TestClient(app) as c:
yield c
app.dependency_overrides.clear()
@pytest.fixture
def sample_item(client):
response = client.post("/items/", json={"name": "Test", "price": 1.0})
return response.json()
# tests/test_items.py
def test_get_item(client, sample_item):
response = client.get(f"/items/{sample_item['id']}")
assert response.status_code == 200
assert response.json()["name"] == "Test"
def test_delete_item(client, sample_item):
response = client.delete(f"/items/{sample_item['id']}")
assert response.status_code == 204
Mock External Services
from unittest.mock import patch, AsyncMock
@patch("app.services.external_api.fetch_data")
def test_with_mocked_api(mock_fetch, client):
mock_fetch.return_value = {"status": "ok", "data": [1, 2, 3]}
response = client.get("/external-data")
assert response.status_code == 200
assert len(response.json()["data"]) == 3
Testing Authentication
@pytest.fixture
def auth_headers(client):
response = client.post("/token", data={
"username": "testuser",
"password": "testpass",
})
token = response.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
def test_protected_route(client, auth_headers):
response = client.get("/me", headers=auth_headers)
assert response.status_code == 200
assert response.json()["username"] == "testuser"
def test_unauthenticated(client):
response = client.get("/me")
assert response.status_code == 401
Async Tests
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
@pytest.mark.asyncio
async def test_async_endpoint():
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as ac:
response = await ac.get("/async-endpoint")
assert response.status_code == 200
Test Coverage
pip install pytest-cov
pytest tests/ --cov=app --cov-report=html
open htmlcov/index.html
Test Organization
tests/
├── conftest.py # shared fixtures
├── test_main.py # root endpoints
├── test_items.py # item CRUD
├── test_auth.py # authentication
└── test_services.py # business logic (no HTTP)
Best Practices
- Test behavior, not implementation — tests should survive refactors
- Use fixtures for setup/teardown — avoid duplication
- One concept per test — clear failure messages
- Test error paths — 404, 422, 401, 500
- Use in-memory SQLite for fast database tests
- Mock external APIs — tests should not depend on network
- Run tests in CI — every pull request
Related
Testing is non-negotiable for production FastAPI applications.