Course topics

By WebNest Studio

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

Python
# 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
Output
.....                                                                    [100%]
5 passed in 0.21s

Testing asynchronously with httpx.AsyncClient

Python
# 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}
Output
.                                                                        [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.