Skip to content

Chapter 20 · Testing FastAPI Applications ​

Chapter goals: Write automated tests for FastAPI apps with TestClient + pytest, master dependency overrides, lifespan testing, and WebSocket/file upload testing.

20.1 TestClient Basics ​

FastAPI is built on Starlette's TestClient (underlying HTTPX) — no real server startup needed, no port occupied, calls the ASGI app directly in-process:

bash
uv add httpx pytest   # TestClient requires httpx
python
# main.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def read_main():
    return {"msg": "Hello World"}
python
# test_main.py
from fastapi.testclient import TestClient
from .main import app  # Relative import when in the same package

client = TestClient(app)  # Pass the app to TestClient


def test_read_main():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"msg": "Hello World"}

Run with pytest. Three key conventions:

  • Test functions use plain def, not async def; no await on the client — this integrates directly with pytest without extra plugins;
  • TestClient usage is identical to httpx: path/query params in the URL, JSON body via json param, form data via data, headers via headers, cookies via cookies;
  • Data sent must be "JSON-serializable objects", not Pydantic models; convert models with jsonable_encoder first if needed.

TIP

You can also from starlette.testclient import TestClient; fastapi.testclient is just a convenience re-export of the same object.

20.2 Organizing Tests with pytest Fixtures ​

Real projects' apps may need lifespan-initialized resources. Put the client in a fixture and use with to trigger the lifespan during the test:

python
import pytest
from fastapi.testclient import TestClient
from .main import app


@pytest.fixture
def client():
    # with statement triggers startup (first half of lifespan) when entering
    # triggers shutdown (second half) when exiting — e.g., closing connection pools
    with TestClient(app) as c:
        yield c


def test_health(client):
    assert client.get("/health").status_code == 200

Without with, TestClient does not run the lifespan — this is the most common cause of "database not initialized in tests."

20.3 dependency_overrides: Replacing Dependencies ​

When testing external services (paid auth APIs, databases), you should not actually call them. FastAPI provides app.dependency_overrides dict: key is the original dependency function, value is the replacement — the original dependency's sub-dependencies also do not execute:

python
# main.py
from fastapi import Depends, FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()


class User(BaseModel):
    username: str


async def verify_token_from_provider(token: str):  # Simulate expensive third-party auth
    raise HTTPException(status_code=401)


async def get_current_user(user=Depends(verify_token_from_provider)):
    return user


@app.get("/me")
async def me(user: User = Depends(get_current_user)):
    return user

In tests:

python
# test_main.py
from fastapi.testclient import TestClient
from .main import app

client = TestClient(app)


def test_me_with_mocked_auth():
    # Override the auth dependency to bypass the real provider
    async def fake_verify_token(token: str = "fake"):
        return {"username": "testuser"}

    app.dependency_overrides[verify_token_from_provider] = fake_verify_token

    response = client.get("/me", headers={"Authorization": "Bearer fake"})
    assert response.status_code == 200
    assert response.json()["username"] == "testuser"

    # Clean up after the test
    app.dependency_overrides.clear()

20.4 Testing WebSocket and File Uploads ​

TestClient supports WebSocket and multipart form data natively:

python
def test_websocket(client: TestClient):
    with client.websocket_connect("/ws") as ws:
        ws.send_text("hello")
        data = ws.receive_text()
        assert data == "Echo: hello"


def test_file_upload(client: TestClient):
    from io import BytesIO
    file_content = b"file contents"
    response = client.post(
        "/upload/",
        files={"file": ("test.txt", BytesIO(file_content), "text/plain")}
    )
    assert response.status_code == 200

20.5 Chapter Summary ​

  • TestClient(app) runs the app in-process without a real server;
  • Use with TestClient(app) as c: to trigger lifespan in tests;
  • app.dependency_overrides replaces dependencies for isolated testing;
  • WebSocket and file upload tests are supported natively.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. Why does TestClient not require a real server?

2. Why use with TestClient(app) instead of client = TestClient(app)?

3. What does app.dependency_overrides do?

4. How do you test a WebSocket endpoint with TestClient?

🛠️ Hands-On Exercises ​

  1. Write tests for a CRUD app: create, read, update, delete — assert status codes and response bodies.
  2. Use dependency_overrides to replace a real DB session with an in-memory dict; verify tests run without a database.
  3. Write a WebSocket test that connects, sends a message, and asserts the echo response.

Proceed to Chapter 21: Configuration Management and Application Lifecycle.