Python Tutorial
Testing FastAPI Applications
Automated tests let you change an API with confidence. FastAPI apps are easy to test: TestClient (built on HTTPX) sends requests straight to the app without starting a server, pytest runs the tests, and app.dependency_overrides swaps real dependencies — the database, the current user, external services — for test versions.
This lesson shows a typical project test suite with fixtures, a separate test database, authentication overrides and parametrised tests.
The Basics
Install pytest (HTTPX is already included in fastapi[standard]). Put tests in files named test_*.py with functions named test_*, create client = TestClient(app), send requests with client.get/post/patch/delete, and assert on response.status_code and response.json(). Run pytest -q. Test the unhappy paths too: 404s, 401s, 409s and validation errors are part of your API contract.
Fixtures and Dependency Overrides
pytest fixtures prepare what tests need and clean up afterwards; a fixture with yield works like a FastAPI yield dependency. app.dependency_overrides[get_session] = get_test_session makes every endpoint use a fresh in-memory test database, and app.dependency_overrides[get_current_user] = lambda: fake_user skips real authentication. Clear the overrides after each test so tests stay independent. For async code, httpx.AsyncClient with ASGITransport and pytest-anyio or pytest-asyncio let tests themselves be async.
Examples
test_api.py — fixtures, overrides and parametrised tests
# test_api.py run with: pytest -q
from typing import Annotated
import pytest
from fastapi import Depends, FastAPI, HTTPException
from fastapi.testclient import TestClient
# ---- the application under test (normally imported from app.main) ----
app = FastAPI()
def get_store(): # the "database" dependency
raise RuntimeError("real database not available in tests")
def get_current_user(): # the auth dependency
raise HTTPException(401, "Not authenticated")
Store = Annotated[dict, Depends(get_store)]
User = Annotated[str, Depends(get_current_user)]
@app.post("/notes", status_code=201)
def create_note(text: str, store: Store, user: User):
note_id = len(store) + 1
store[note_id] = {"text": text, "owner": user}
return {"id": note_id, **store[note_id]}
@app.get("/notes/{note_id}")
def read_note(note_id: int, store: Store, user: User):
if note_id not in store:
raise HTTPException(404, "Note not found")
return store[note_id]
# ---- tests ----
@pytest.fixture
def store():
return {}
@pytest.fixture
def client(store):
app.dependency_overrides[get_store] = lambda: store
app.dependency_overrides[get_current_user] = lambda: "asha"
yield TestClient(app)
app.dependency_overrides.clear() # keep tests independent
def test_create_and_read_note(client):
created = client.post("/notes?text=Buy milk")
assert created.status_code == 201
assert created.json() == {"id": 1, "text": "Buy milk", "owner": "asha"}
assert client.get("/notes/1").json()["text"] == "Buy milk"
def test_missing_note_returns_404(client):
response = client.get("/notes/99")
assert response.status_code == 404
assert response.json() == {"detail": "Note not found"}
def test_requires_authentication(store):
app.dependency_overrides[get_store] = lambda: store # no user override this time
response = TestClient(app).post("/notes?text=x")
app.dependency_overrides.clear()
assert response.status_code == 401
@pytest.mark.parametrize("url, expected", [
("/notes/abc", 422), # invalid path parameter
("/notes", 405), # GET not allowed on the collection
])
def test_bad_requests(client, url, expected):
assert client.get(url).status_code == expected
..... [100%]
5 passed in 0.21s
Testing asynchronously with httpx.AsyncClient
# pip install pytest anyio
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
app = FastAPI()
@app.get("/ping")
async def ping():
return {"pong": True}
@pytest.mark.anyio
async def test_ping():
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
response = await ac.get("/ping")
assert response.json() == {"pong": True}
. [100%]
1 passed in 0.05s
Common Mistakes
- Testing against the real production or development database.
- Forgetting to clear dependency_overrides, so one test changes the behaviour of the next.
- Only testing the happy path and never checking 401, 404, 409 and 422 responses.
- Sharing state between tests through module-level variables instead of fixtures.
- Starting a real server with uvicorn for tests when TestClient can call the app directly.
Key Points to Remember
- TestClient sends requests to the app in-process; pytest runs and reports the tests.
- Fixtures prepare clients, databases and data, and clean up with yield.
- app.dependency_overrides swaps databases, users and external services in tests.
- Parametrised tests cover many inputs and error cases compactly.
- httpx.AsyncClient with ASGITransport tests async code from async tests.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local project environment for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.