# 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 ```python @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 ```python 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 ```python @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 ```python 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.