FastAPI Test Services¶
Building testable FastAPI microservices and writing tests against them. TestClient for synchronous tests, dependency injection overrides, and running real test servers.
TestClient (In-Process)¶
from fastapi import FastAPI, Depends
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/api/users/{user_id}")
def get_user(user_id: int):
return {"id": user_id, "name": "Alice"}
@pytest.fixture
def client():
return TestClient(app)
def test_get_user(client):
resp = client.get("/api/users/1")
assert resp.status_code == 200
assert resp.json()["name"] == "Alice"
TestClient runs the app in-process (no network). Fast, synchronous, ideal for unit tests.
Dependency Injection Override¶
from fastapi import Depends
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/api/users")
def list_users(db=Depends(get_db)):
return db.query(User).all()
# In tests: override the dependency
@pytest.fixture
def client():
def override_get_db():
db = TestSessionLocal()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app)
app.dependency_overrides.clear()
This replaces the real DB with test DB without modifying application code.
Async TestClient¶
import httpx
import pytest_asyncio
@pytest_asyncio.fixture
async def async_client():
async with httpx.AsyncClient(app=app, base_url="http://test") as client:
yield client
@pytest.mark.asyncio
async def test_async_endpoint(async_client):
resp = await async_client.get("/api/users")
assert resp.status_code == 200
Required for testing async endpoints that use await.
Building a Test Microservice¶
# test_service/app.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
users_db = {}
class User(BaseModel):
name: str
email: str
@app.post("/api/users", status_code=201)
def create_user(user: User):
user_id = len(users_db) + 1
users_db[user_id] = user.model_dump()
users_db[user_id]["id"] = user_id
return users_db[user_id]
@app.get("/api/users/{user_id}")
def get_user(user_id: int):
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
return users_db[user_id]
@app.get("/api/users")
def list_users():
return list(users_db.values())
Testing with Real Server (uvicorn)¶
import subprocess
import time
@pytest.fixture(scope="session")
def live_server():
proc = subprocess.Popen(
["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"],
stdout=subprocess.PIPE,
)
wait_for_port(8000, timeout=10)
yield "http://localhost:8000"
proc.terminate()
proc.wait()
def test_live_endpoint(live_server):
resp = requests.get(f"{live_server}/api/users")
assert resp.status_code == 200
Request Validation Testing¶
@pytest.mark.parametrize("payload,expected_status", [
({"name": "Alice", "email": "[email protected]"}, 201),
({"name": "", "email": "[email protected]"}, 422), # empty name
({"name": "Alice"}, 422), # missing email
({"name": "Alice", "email": "not-an-email"}, 422), # invalid email
({}, 422), # empty body
])
def test_create_user_validation(client, payload, expected_status):
resp = client.post("/api/users", json=payload)
assert resp.status_code == expected_status
Middleware and Event Testing¶
@app.middleware("http")
async def add_request_id(request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid4()))
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
def test_request_id_header(client):
resp = client.get("/api/users", headers={"X-Request-ID": "test-123"})
assert resp.headers["X-Request-ID"] == "test-123"
def test_request_id_generated(client):
resp = client.get("/api/users")
assert "X-Request-ID" in resp.headers
Gotchas¶
-
Issue:
TestClientdoes not runlifespanevents (startup/shutdown) by default. Fix: Usewith TestClient(app) as client:context manager - this triggers lifespan events. -
Issue:
dependency_overridesis global state - one test's override affects another. Fix: Always clear overrides in fixture teardown:app.dependency_overrides.clear(). Use function-scoped fixtures. -
Issue: Async endpoints tested with sync
TestClientwork but lose async benefits. Fix: Usehttpx.AsyncClientwithpytest-asynciofor true async testing. Especially important if endpoints useawaitinternally.