flowCreate.solutions

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 (usually 403) 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:

  • client exists in tests/conftest.py and is bound to your ASGI app (see Generic Example - Conftest).
  • auth_headers exists in tests/conftest.py (reference backend builds this via a real login flow).
  • tenant_id exists in tests/conftest.py (or equivalent “scope id” for your app).
  • sample_item exists in tests/{module}/fixtures.py and 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.