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.

Create a free account to save this page, take notes and track your progress. Sign in

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

API tests with TestClient and dependency overrides
# 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.

Was this page helpful?