Warming up the neural circuits...
By the end of this chapter you will:
Untested APIs feel fast until change arrives. Then every refactor becomes guesswork.
tests are not only for correctness. They protect:
The goal is not maximum test count. The goal is high-signal coverage aligned to risk.
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health() -> None:
response = client.get("/health")
assert response.status_code == 200
assert response.json()["status"] == "ok"TestClient runs the app in-process, making tests fast and deterministic.
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.api.deps import get_db
class FakeDB:
pass
@pytest.fixture
def client():
fake_db = FakeDB()
def
Two practical layers:
Use both layers when possible. Different failures appear in each.
import pytest
@pytest.mark.parametrize(
"payload,expected_status",
[
({"email": "ava@example.com", "name": "Ava"}, 201),
({"email": "not-an-email"
Test not only success paths:
If you only test happy paths, production will test the unhappy paths for you.
Prioritize by blast radius:
Coverage percentage alone is not enough. Risk alignment matters more.
Prefer tests that validate externally visible behavior: status codes, payload contracts, and side effects. Avoid brittle tests tied to private implementation details.
| Endpoint category | Must-have assertions | Why |
|---|---|---|
| Auth endpoints | token issuance, bad credentials, lockout/rate policy | Security critical |
| Resource creation | 201 success, 422 invalid payload, 409 conflict | Core business flow |
| Protected routes | 401 no token, 403 wrong role, 200 allowed role | Access control correctness |
| Read endpoints | shape stability and behavior | Client compatibility |
| Update/delete | idempotency and not-found behavior | Correct transitions |
| Mistake | Why it hurts | Better move |
|---|---|---|
| Testing only happy path | Bugs hide in error branches | Add negative-path scenarios |
| Shared mutable fixture state | Flaky and order-dependent tests | Isolate state per test |
| Hard-coded fragile ids/timestamps | Non-deterministic failures | Use factories and stable clocks where needed |
| Mocking too deep in API tests | False confidence with unrealistic behavior | Keep contract-level tests near real boundaries |
| Chasing 100% coverage blindly | Time spent on low-value assertions | Prioritize by risk and blast radius |
FastAPI app with status endpoint200 response with expected JSONThree payload variants201 for valid, 422 for invalid variantsget_db dependencyTest suite runs without production DB/admin route401, 403, 200 outcomesCreate and delete endpoints409 on duplicate create, stable delete semanticsBeginner:
"Why use TestClient instead of calling endpoint functions directly?"
TestClient exercises the full HTTP layer including routing, validation, , and dependencies.
"What is the difference between 401 and 403 in tests?"
401 means authentication is missing/invalid. 403 means authenticated but lacking permission.
Senior:
"How do you balance speed and realism in API test strategy?"
Keep fast contract tests for every PR, then add container-backed integration tests for dialect and infra-sensitive behavior.
"What anti-pattern leads to flaky API test suites most often?"
Shared mutable state across tests and non-deterministic setup/teardown boundaries.
TestClient validates request/response behavior
Fixtures isolate state and dependencies
Always test auth, validation, and error paths
Coverage quality > coverage percentageWhat is the key benefit of FastAPI TestClient in API tests?
Dependency overrides let you test route behavior without touching production resources.
Factories make setup intent clearer than hand-built nested dictionaries.