Files
2026-06-01 21:42:05 +00:00

4.7 KiB

Testing

Test FastAPI applications at the HTTP or ASGI boundary whenever practical. Prefer client-based tests, dependency overrides, and explicit lifespan or session fixtures over patching internals.

Why this convention exists

FastAPI does a lot of important work around your code:

  • request parsing
  • dependency injection
  • auth wiring
  • validation
  • error serialization
  • startup and shutdown behavior

If tests bypass those seams, they skip the behavior the framework is responsible for.

The repeated pattern across FastAPI, Starlette, HTTPX, and production templates is:

  • drive the app through a client
  • make fixture lifetime explicit
  • override dependencies at the framework seam
  • exercise lifespan behavior deliberately

The convention

  1. Use TestClient or HTTPX ASGI clients for route tests.
  2. Put shared app, client, and session setup in fixtures.
  3. Use app.dependency_overrides to replace request-time collaborators.
  4. Use yield fixtures when cleanup matters.
  5. Enter lifespan-aware client contexts when startup or shutdown behavior is part of the test.

Follow it for boundary behavior

This is the default for:

  • route tests
  • auth and permission tests
  • validation and error-contract tests
  • startup and shutdown behavior
  • request and response schema behavior

Pure business logic is different: if the code is framework-agnostic, test it directly as normal Python.

Preferred shapes

Client fixture with explicit lifetime

@pytest.fixture
def client() -> Generator[TestClient, None, None]:
    with TestClient(app) as client:
        yield client

This makes startup and teardown behavior visible.

Dependency overrides for request-time seams

app.dependency_overrides[get_current_user] = fake_user
try:
    response = client.get("/items")
finally:
    app.dependency_overrides = {}

This swaps collaborators where FastAPI expects them to be swapped.

Explicit DB or session cleanup fixture

@pytest.fixture(scope="session", autouse=True)
def db() -> Generator[Session, None, None]:
    with Session(engine) as session:
        init_db(session)
        yield session
        cleanup_db(session)
        session.commit()

This makes resource lifetime and cleanup rules obvious.

Counterexamples

Monkeypatch soup

monkeypatch.setattr("app.api.routes.items.session", fake_session)
monkeypatch.setattr("app.api.routes.items.get_user", fake_user)

Bad because the test couples to route internals instead of public seams.

Testing startup-sensitive behavior without entering client context

If the app depends on startup-initialized resources, a client used without the proper lifespan context can produce misleading green tests.

Calling route functions directly for integration-style assertions

That skips request parsing, dependency injection, validation, and error serialization, which is most of what FastAPI is doing for you.

Source signals

FastAPI

  • tests/test_dependency_security_overrides.py:24-29 defines a route whose collaborators come from dependency injection.
  • tests/test_dependency_security_overrides.py:44-63 overrides those collaborators with app.dependency_overrides and then resets the override map.

Starlette

  • tests/test_applications.py:160-163 uses a yielded client fixture around a test client context manager.
  • tests/test_applications.py:234-238 uses raise_server_exceptions=False so tests can inspect a 500 response instead of immediately re-raising the server exception.
  • tests/test_applications.py:394-409 verifies lifespan startup before yield and cleanup after yield through the client context.
  • starlette/applications.py:46-48 explicitly documents lifespan as the preferred startup and shutdown mechanism.

HTTPX

  • httpx/_transports/asgi.py:63-83 exposes ASGITransport specifically for driving an ASGI app in-process, including testing-oriented constructor options.
  • httpx/_transports/asgi.py:78-81 documents raise_app_exceptions=False for inspecting application failures as responses.
  • httpx/_transports/mock.py:15-43 exposes MockTransport as an explicit transport seam when testing HTTP clients.

Full-stack FastAPI Template

  • backend/tests/conftest.py:15-24 keeps DB setup and cleanup explicit in a yielded session fixture.
  • backend/tests/conftest.py:27-30 defines a TestClient fixture with a visible context boundary.
  • backend/tests/conftest.py:33-42 derives auth fixtures from the client instead of bypassing the app boundary.

Bottom line

Test the app where FastAPI actually does work:

  • through the client
  • through dependencies
  • through lifespan
  • with explicit fixture lifetime

Patch internals only when there is no better seam.