Testing FastAPI Applications
Introduction
pytest plus FastAPI’s TestClient simulate HTTP requests without binding a port. dependency_overrides swap database and auth for isolated tests. This chapter covers fixtures, CRUD tests, authenticated routes, and optional async testing with httpx.
Prerequisites
- SQLAlchemy With FastAPI
- Authentication and JWT
pip install pytest httpx
Install and Run
pip install pytest httpx
pytest
pytest -v tests/test_items.pytests/conftest.py structure:
Code explanation:
- In-memory SQLite with
StaticPoolshares one connection for tests dependency_overridesreplacesget_dbfor all requests in test client
Compare Flask testing.
Test Health and JSON
tests/test_health.py:
def test_health(client):
response = client.get("/health")
assert response.status_code == 200
assert response.json()["status"] == "ok"Test CRUD
Test Authentication
Login uses form data when using OAuth2PasswordRequestForm.
Override Current User
from app.api.deps import get_current_user
from app.models.user import User
def test_admin_route(client):
fake_admin = User(id=1, username="admin", hashed_password="x", role="admin")
app.dependency_overrides[get_current_user] = lambda: fake_admin
response = client.get("/api/v1/admin/stats")
app.dependency_overrides.pop(get_current_user, None)
assert response.status_code == 200Clear overrides after each test to avoid leakage.
Validation Error Tests
def test_create_item_invalid_price(client):
response = client.post("/api/v1/items/", json={"name": "Bad", "price": -1})
assert response.status_code == 422
assert "detail" in response.json()Settings Override
from app.core.config import get_settings, Settings
def get_settings_override():
return Settings(secret_key="test", database_url="sqlite://")
app.dependency_overrides[get_settings] = get_settings_overrideAsync Tests (Optional)
pip install pytest-asyncioimport pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
@pytest.mark.asyncio
async def test_root_async():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
response = await ac.get("/")
assert response.status_code == 200Use for async def routes and async dependencies.
Coverage
pip install pytest-cov
pytest --cov=app --cov-report=term-missingLink CI to Git and CI/CD—Docker and CI chapter.
Tip
Test Behavior, Not OpenAPI Snapshot
Focus on status codes and business rules unless you intentionally snapshot API contracts.
FAQ
TestClient vs live server?
TestClient is faster and deterministic—no port conflicts.
Database not isolated?
Ensure drop_all in fixture teardown; avoid shared file SQLite path.
422 vs assertion?
Inspect response.json()["detail"] for field errors.
Lifespan events in tests?
TestClient triggers lifespan on enter/exit when used as context manager.
Parallel pytest workers?
Each worker needs isolated DB—use in-memory SQLite per worker.
Mock external HTTP?
Use respx or unittest.mock.patch on httpx clients.