Learn / Python and FastAPI Backend Engineering
Testing, Packaging, and Operations
Package a FastAPI service so reviewers can prove route contracts, dependency overrides, startup commands, and liveness/readiness behavior before traffic reaches it.
Course: Python and FastAPI Backend Engineering. Level: Intermediate. Topic: Backend craft.
Stage: intermediate - Intermediate verification - FastAPI testing, packaging, and runtime checks. Turn local API code into a repeatable service package with tests, health checks, and startup evidence.
Outcomes
- Test success, validation, auth, and repository-failure paths through the ASGI boundary.
- Package the service with repeatable startup and environment proof instead of laptop-only commands.
- Split liveness from readiness so deploys know when the process is alive and when dependencies can receive traffic.
Concepts
- ASGI contract test
- dependency override fixture
- startup proof
- liveness/readiness split
Concept flow
Show how route contract tests, package startup, and health endpoints become deploy evidence for a production service.
- pytest
- ASGI client
- FastAPI app
- Override fixture
- Fake repository
- Health endpoints
- Deploy gate
Session flow
- Test through the real API boundary (concept, 14 min) — Use an ASGI client to prove request parsing, validation, dependency resolution, response models, and expected failure behavior.
- Handler-only tests miss the boundary most clients experience.
- A useful route contract test includes status, body shape, and one important side effect or fake interaction.
- Make dependency overrides boring and isolated (walkthrough, 18 min) — Install fake repositories, auth, and settings through fixtures that clean up app-level override state.
- Override fixtures should be scoped, named, and cleared.
- Repository failures deserve client-safe responses and operator-facing logs.
- Package startup as review evidence (exercise, 12 min) — Document the clean install command, run command, required env names, and captured startup/readiness output.
- A deployable service has a repeatable command, not a memory of how one laptop ran it.
- Startup proof should be safe to paste into a pull request without secrets.
- Split liveness from readiness (exercise, 10 min) — Keep liveness cheap and make readiness prove required dependencies before traffic admission.
- Liveness restarts broken processes; readiness protects users from instances that are not ready.
- Readiness should fail closed when a required dependency is unavailable.
Code example
Python in tests/test_items.py.
from collections.abc import AsyncIterator
from dataclasses import dataclass
import pytest
from fastapi import APIRouter, Depends, FastAPI, HTTPException, status
from httpx import ASGITransport, AsyncClient
from pydantic import BaseModel, Field
pytestmark = pytest.mark.anyio
class ItemCreate(BaseModel):
name: str = Field(min_length=2, max_length=80)
quantity: int = Field(ge=0, le=500)
class ItemRead(BaseModel):
id: str
name: str
quantity: int
class ItemRepository:
async def create(self, payload: ItemCreate) -> ItemRead:
raise NotImplementedError
async def ready(self) -> bool:
raise NotImplementedError
@dataclass
class FakeItemRepository(ItemRepository):
fail_writes: bool = False
ready_state: bool = True
created_count: int = 0
async def create(self, payload: ItemCreate) -> ItemRead:
if self.fail_writes:
raise RuntimeError("database unavailable")
self.created_count += 1
return ItemRead(id="item_123", name=payload.name, quantity=payload.quantity)
async def ready(self) -> bool:
return self.ready_state
async def get_item_repository() -> ItemRepository:
return ItemRepository()
router = APIRouter(prefix="/items", tags=["items"])
@router.post("", response_model=ItemRead, status_code=status.HTTP_201_CREATED)
async def create_item(
payload: ItemCreate,
repository: ItemRepository = Depends(get_item_repository),
) -> ItemRead:
try:
return await repository.create(payload)
except RuntimeError as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Items are temporarily unavailable.",
) from exc
def create_app() -> FastAPI:
app = FastAPI(title="Items API", version="2026.06.30")
app.include_router(router)
@app.get("/health/live", include_in_schema=False)
async def live() -> dict[str, str]:
return {"status": "alive"}
@app.get("/health/ready", include_in_schema=False)
async def ready(repository: ItemRepository = Depends(get_item_repository)) -> dict[str, str]:
if not await repository.ready():
raise HTTPException(status_code=503, detail="Database is not ready.")
return {"status": "ready"}
return app
@pytest.fixture
async def client_with_fake_repo() -> AsyncIterator[tuple[AsyncClient, FakeItemRepository]]:
app = create_app()
fake_repo = FakeItemRepository()
app.dependency_overrides[get_item_repository] = lambda: fake_repo
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://testserver",
) as client:
yield client, fake_repo
app.dependency_overrides.clear()
async def test_create_item_contract(client_with_fake_repo):
client, fake_repo = client_with_fake_repo
response = await client.post("/items", json={"name": "Notebook", "quantity": 3})
assert response.status_code == 201
assert response.json() == {"id": "item_123", "name": "Notebook", "quantity": 3}
assert fake_repo.created_count == 1
async def test_validation_contract(client_with_fake_repo):
client, _fake_repo = client_with_fake_repo
response = await client.post("/items", json={"name": "N", "quantity": -1})
assert response.status_code == 422
assert response.json()["detail"][0]["loc"][0] == "body"
async def test_repository_failure_is_client_safe(client_with_fake_repo):
client, fake_repo = client_with_fake_repo
fake_repo.fail_writes = True
response = await client.post("/items", json={"name": "Notebook", "quantity": 3})
assert response.status_code == 503
assert response.json()["detail"] == "Items are temporarily unavailable."
async def test_liveness_and_readiness_are_separate(client_with_fake_repo):
client, fake_repo = client_with_fake_repo
assert (await client.get("/health/live")).status_code == 200
fake_repo.ready_state = False
response = await client.get("/health/ready")
assert response.status_code == 503
assert response.json()["detail"] == "Database is not ready."
Walkthrough examples
- Deployable FastAPI service proof pack — A team wants to merge a new item API only if another engineer can test it, start it, and prove it should receive traffic.
- File: app/main.py
- File: app/items/routes.py
- File: app/items/dependencies.py
- File: tests/test_items_contract.py
- File: Dockerfile
- File: docs/ops/startup-proof.md
- Create create_app() so tests and production use the same router and provider graph.
- Write ASGI route tests for success, validation failure, auth failure, repository failure, liveness, and readiness.
- Add dependency override fixtures for auth, repository, and settings; clear overrides in teardown.
- Document uvicorn or container startup with required environment names and a captured readiness response.
- Add a review checklist that names the command evidence required before deploy.
Practice
- Write route tests for success, validation failure, auth failure, and repository failure through ASGITransport or TestClient.
- Build a fixture that installs dependency overrides and clears them after the test scope.
- Run the package startup command from a clean shell and save the exact command plus the first successful log line.
- Add /health/live and /health/ready with different cost and traffic meanings.
Checklist
- Route tests hit the app boundary instead of calling handlers directly.
- Dependency overrides are installed by fixtures and cleared after each test.
- Validation and repository failures return stable client-safe payloads.
- Startup proof names required environment variables and the run command.
- Liveness is cheap, readiness checks required dependencies, and deploy gates use readiness.
- Review notes include pytest output, startup output, and readiness output.
Quiz prompts
- Why test a FastAPI route through an ASGI client instead of calling the route function directly? — Route contract tests should cover the actual web boundary clients depend on, not only the Python function body.
- What should a dependency override fixture always do after the test? — FastAPI overrides are app-level state. Cleanup is part of the test contract.
- Which signal belongs in readiness rather than liveness? — Liveness asks whether the process is alive. Readiness asks whether the instance should receive traffic now.
- A clean startup proof should include which evidence? — Startup proof should be reproducible by another engineer without inheriting your local shell state.
- What is the best response when the repository dependency fails during a create route? — Clients need stable status semantics; operators need internal logs. Do not leak implementation details in the public payload.
Flashcards
- What does an ASGI route test prove? It proves the HTTP boundary: routing, validation, dependencies, serialization, and error mapping.
- Why clear FastAPI dependency overrides? Overrides are app-level state. Clearing them prevents fake auth, repositories, or settings from leaking into later tests.
- How is readiness different from liveness? Liveness says the process can answer. Readiness says the instance has required dependencies and can safely receive traffic.
- What belongs in startup proof? Install/run command, required env names, version or startup log, and readiness output without secrets.
- Why map repository outages to a stable 503? Clients get predictable retry semantics while internal exception details stay in logs and traces.
- What is the review smell in calling route functions directly for every test? It bypasses the web boundary, so validation, dependencies, response models, and exception handlers can drift untested.
Labs
- Ship a tested and startable FastAPI package — Turn a working local route into a production proof pack: boundary tests, isolated overrides, clean startup command, and health/readiness evidence.
- Expose create_app() and use it in both tests and the production run command.
- Write ASGI tests for create success, validation failure, auth failure, repository failure, /health/live, and /health/ready.
- Create fake auth and repository override fixtures that clear app.dependency_overrides after each test.
- Add a Dockerfile or runbook command that starts the app from a clean environment.
- Capture pytest output, startup output, and readiness output in docs/ops/startup-proof.md.
- The test suite proves route contracts and expected failure responses through the app boundary.
- A teammate can run the documented startup command without relying on private shell aliases.
- Readiness fails when the fake dependency is unavailable and passes when it recovers.
- The review note lists required env names but no secret values.
Challenge
- Review-ready FastAPI operations proof pack (Core) — Create the evidence a reviewer needs before approving deploy: route contract tests, isolated overrides, reproducible startup command, liveness/readiness endpoints, and a short traffic-admission note.
- Tests cover success, validation failure, auth failure, repository failure, liveness, and readiness.
- Dependency override fixtures are scoped and cleaned up.
- Startup proof includes command, env names, version or log output, and readiness response.
- Liveness and readiness have different meanings and the deploy note names which one receives traffic checks.
Canonical lesson URL