Generic Example — API Tests
API tests validate routing, dependencies, authentication, and response contracts by calling your FastAPI endpoints through an httpx.AsyncClient bound to the ASGI app (no real network).
What API tests should cover
- Happy path responses (status, body shape, key fields).
- Auth: unauthenticated (usually
401) and unauthorized/forbidden (usually403) cases where relevant. - Validation: malformed payloads should return
422(Pydantic validation). - Not found: invalid ids should return
404(if your API uses 404 for missing entities).
Prerequisites (fixtures)
These examples assume:
clientexists intests/conftest.pyand is bound to your ASGI app (seeGeneric Example - Conftest).auth_headersexists intests/conftest.py(reference backend builds this via a real login flow).tenant_idexists intests/conftest.py(or equivalent “scope id” for your app).sample_itemexists intests/{module}/fixtures.pyand creates a persisted entity.
Template: tests/{module}/test_apis.py
import pytest
from httpx import AsyncClient
pytestmark = pytest.mark.asyncio
async def test_create_item_happy_path(client: AsyncClient, auth_headers: dict[str, str], tenant_id: str):
response = await client.post(
"/api/v1/items",
headers={**auth_headers, "X-Tenant-Id": tenant_id},
json={"name": "API Item", "price_cents": 50},
)
assert response.status_code == 200, response.text
payload = response.json()
# Prefer a stable response contract (adapt keys to your API conventions)
assert payload["success"] is True
assert payload["content"]["item"]["name"] == "API Item"
async def test_create_item_unauthenticated(client: AsyncClient, tenant_id: str):
response = await client.post(
"/api/v1/items",
headers={"X-Tenant-Id": tenant_id},
json={"name": "API Item", "price_cents": 50},
)
# If your app returns 403 for missing credentials, update this expectation.
assert response.status_code == 401
async def test_get_item_by_id_happy_path(client: AsyncClient, auth_headers: dict[str, str], tenant_id: str, sample_item):
response = await client.get(
f"/api/v1/items/{sample_item.id}",
headers={**auth_headers, "X-Tenant-Id": tenant_id},
)
assert response.status_code == 200, response.text
payload = response.json()
assert payload["content"]["item"]["id"] == sample_item.id
Common pitfalls
- Not using an ASGI-bound client: API tests should not make real HTTP calls to localhost or external services.
- Asserting the entire JSON payload: assert the contract + key fields; avoid brittle full-payload comparisons unless the API is strictly stable.
- Skipping negative cases: unauthenticated + validation failures catch a lot of regressions early.