Testing FastAPI Apps

TestClient, and a complete endpoint test using a mocked dependency via dependency_overrides.

TestClient

FastAPI's TestClient (built on httpx) lets you call your app's endpoints directly in a test, with no running server or network involved — requests go straight through the same ASGI app your real deployment runs.

Bash
pip install httpx pytest
Python
# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
    return {"item_id": item_id, "q": q}
Python
# test_main.py
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_read_item():
    response = client.get("/items/42?q=test")
    assert response.status_code == 200
    assert response.json() == {"item_id": 42, "q": "test"}

def test_read_item_invalid_id_returns_422():
    response = client.get("/items/not-an-int")
    assert response.status_code == 422
Bash
pytest

TestClient(app) wraps your actual FastAPI instance — every test request goes through the real routing, dependency resolution, and Pydantic validation exactly as a live request would, just without opening a real TCP socket.

A complete test with a mocked dependency

Testing an endpoint that depends on a database session or an external service is where Depends() pays off directly — FastAPI lets you swap out any dependency for a fake one during tests via app.dependency_overrides, with zero changes to the endpoint code itself.

Python
# main.py
from fastapi import Depends, FastAPI

app = FastAPI()

def get_db():
    db = RealDatabaseSession()
    try:
        yield db
    finally:
        db.close()

@app.get("/users/{user_id}")
def read_user(user_id: int, db=Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    if user is None:
        return {"error": "not found"}
    return {"id": user.id, "name": user.name}
Python
# test_main.py
from fastapi.testclient import TestClient
from main import app, get_db

class FakeUser:
    def __init__(self, id, name):
        self.id = id
        self.name = name

class FakeDB:
    def __init__(self, users):
        self._users = {u.id: u for u in users}

    def query(self, model):
        return self   # a tiny stand-in supporting only .filter().first()

    def filter(self, *args, **kwargs):
        return self

    def first(self):
        return next(iter(self._users.values()), None)

def override_get_db():
    yield FakeDB([FakeUser(id=1, name="Ada Lovelace")])

app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)

def test_read_existing_user():
    response = client.get("/users/1")
    assert response.status_code == 200
    assert response.json() == {"id": 1, "name": "Ada Lovelace"}

def test_read_missing_user():
    app.dependency_overrides[get_db] = lambda: iter([FakeDB([])])
    response = client.get("/users/999")
    assert response.json() == {"error": "not found"}

app.dependency_overrides is a dict keyed by the original dependency function — assigning app.dependency_overrides[get_db] = override_get_db tells FastAPI to call override_get_db instead of get_db for the lifetime of that override, for every request the test client makes. This is the standard way to substitute a real database (or a real external HTTP call) with a fast, in-memory fake for tests, without touching the endpoint's own code at all — the endpoint still just writes db=Depends(get_db).

Common mistakes

  • Testing against a real database or a real third-party API instead of overriding the dependency — tests become slow, flaky, and dependent on network/service availability instead of pure and repeatable.
  • Forgetting to reset app.dependency_overrides between tests (or between test modules) — an override left in place from one test can silently affect a later test that expected the real dependency.
  • Asserting only on status_code and skipping response.json() — a 200 with the wrong body still passes a status-only test while genuinely being broken.

Interview questions

Q: How does TestClient let you test a FastAPI app without running a real server? It wraps your ASGI application directly and simulates HTTP requests against it in-process, running the exact same routing, dependency resolution, and validation code a real deployed request would hit — no TCP socket, port, or running Uvicorn process is involved. This makes tests fast and self-contained while still exercising real framework behavior, not just your bare Python functions.

Q: How do you test an endpoint that depends on a database, without hitting a real database in your test suite? Override the dependency function via app.dependency_overrides[get_db] = fake_get_db, where fake_get_db yields an in-memory fake or a test-database session instead of a real production connection. Because the endpoint only ever asks for whatever Depends(get_db) resolves to, it runs unmodified against the fake — the substitution happens entirely at the dependency-injection layer, not inside the endpoint's own code.