Testing in Practice · Testing APIs, Contracts and User Journeys
Testing HTTP APIs Through the Framework
Test web APIs through their HTTP layer with FastAPI's TestClient: status codes, validation errors, authentication and overridden dependencies.
What you'll build
Tests for a small FastAPI orders API, sent through the real routing, validation and dependency injection: a successful create, a validation error (422), an authentication failure (401), and a dependency override that swaps the payment gateway for a fake.
The idea
- Test APIs through HTTP requests in-process (
TestClient), not by calling handler functions: routing, validation, serialisation and auth are part of the behaviour. - Override dependencies (database sessions, external clients, current user) with fakes for each test.
- Assert on the contract clients see: status codes, response bodies and important headers.
The code
# file: app.py
from fastapi import Depends, FastAPI, Header, HTTPException
from pydantic import BaseModel, Field
app = FastAPI()
class OrderIn(BaseModel):
sku: str = Field(min_length=1)
quantity: int = Field(ge=1, le=10)
class PaymentGateway:
def charge(self, amount_cents: int) -> str:
raise RuntimeError("calls the real payment provider")
def get_gateway() -> PaymentGateway:
return PaymentGateway()
def current_user(authorization: str = Header(default="")) -> str:
if authorization != "Bearer test-token":
raise HTTPException(status_code=401, detail="invalid token")
return "ana"
@app.post("/orders", status_code=201)
def create_order(order: OrderIn, user: str = Depends(current_user), gateway: PaymentGateway = Depends(get_gateway)):
charge_id = gateway.charge(order.quantity * 850)
return {"user": user, "sku": order.sku, "quantity": order.quantity, "charge": charge_id}
# file: test_app.py
import pytest
from fastapi.testclient import TestClient
from app import app, get_gateway
class FakeGateway:
def __init__(self):
self.charges = []
def charge(self, amount_cents):
self.charges.append(amount_cents)
return f"ch_{len(self.charges)}"
@pytest.fixture
def gateway():
fake = FakeGateway()
app.dependency_overrides[get_gateway] = lambda: fake
yield fake
app.dependency_overrides.clear()
@pytest.fixture
def client():
return TestClient(app)
AUTH = {"Authorization": "Bearer test-token"}
def test_creates_an_order_and_charges_once(client, gateway):
r = client.post("/orders", json={"sku": "mug", "quantity": 2}, headers=AUTH)
assert r.status_code == 201
assert r.json() == {"user": "ana", "sku": "mug", "quantity": 2, "charge": "ch_1"}
assert gateway.charges == [1700]
def test_rejects_invalid_quantity(client, gateway):
r = client.post("/orders", json={"sku": "mug", "quantity": 50}, headers=AUTH)
assert r.status_code == 422
assert r.json()["detail"][0]["loc"] == ["body", "quantity"]
assert gateway.charges == []
def test_requires_authentication(client, gateway):
assert client.post("/orders", json={"sku": "mug", "quantity": 1}).status_code == 401
# file: main.py
import pytest
pytest.main(["-q", "--tb=short", "-p", "no:cacheprovider",
"-W", "ignore::starlette.exceptions.StarletteDeprecationWarning"]) # a client-library notice, not our code
Output
... [100%] 3 passed in <time>
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Calling handler functions directly, skipping validation, auth and serialisation.
- Leaving dependency overrides in place after a test (clear them in fixture teardown).
- Only testing 200 responses; error responses are part of the API contract too.
Key points
- Test APIs through HTTP in-process.
- Override dependencies with fakes per test.
- Assert on status codes and bodies for success and error cases.
Further reading: FastAPI: testing · FastAPI: testing dependencies with overrides
Practice questions
Why test an API through TestClient instead of calling handlers?
Answer
Routing, request validation, authentication, dependency injection and serialisation all run, so the test checks what clients actually experience.
What are dependency overrides used for in tests?
Answer
Replacing dependencies such as payment clients, databases or the current user with fakes for a test, without changing application code.
Why test error responses?
Answer
Clients depend on status codes and error formats too; a change from 422 to 500 breaks them just as much as a change to success responses.
Open in the app to mark lessons complete, take notes and track your progress.