测试是保证 API 质量的关键手段。FastAPI 基于 Starlette 提供了开箱即用的 TestClient,让你无需启动真实服务器就能对接口进行端到端测试。它与 pytest 无缝集成,支持依赖覆写、异步路由测试、WebSocket 测试等高级场景。本文从零开始,完整梳理 FastAPI 测试的核心知识点与最佳实践。

为什么用 TestClient

传统 Web 框架测试通常需要先启动服务、再发送 HTTP 请求,流程繁琐且容易引入环境问题。TestClient 直接包装了 ASGI 应用实例,在进程内发起请求,无需绑定端口、无需网络栈——速度快、隔离性好、CI 友好。

它的底层是 httpx 的同步客户端,API 风格与 requests 库高度一致,上手成本极低。

环境准备

测试依赖需要安装 httpxpytest

1
pip install httpx pytest

被测应用保持简单结构,便于测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# main.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/hello")
async def hello():
return {"message": "Hello, World!"}


@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {"user_id": user_id, "name": f"用户 {user_id}"}

第一个测试

使用 TestClient 只需三步:创建客户端实例、发起请求、断言响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# test_main.py
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)


def test_hello():
response = client.get("/hello")
assert response.status_code == 200
assert response.json() == {"message": "Hello, World!"}


def test_get_user():
response = client.get("/users/42")
assert response.status_code == 200
data = response.json()
assert data["user_id"] == 42
assert "用户 42" in data["name"]

运行测试:

1
pytest test_main.py -v

TestClient 暴露的方法与 HTTP 方法一一对应:client.get()client.post()client.put()client.delete()client.patch()client.options()client.head()

测试 POST / 请求体

需要提交 JSON 请求体时,使用 json 参数;提交表单数据用 data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from pydantic import BaseModel
from fastapi import FastAPI

app = FastAPI()


class Item(BaseModel):
name: str
price: float


@app.post("/items")
async def create_item(item: Item):
return {"name": item.name, "price_with_tax": round(item.price * 1.13, 2)}


# 测试
def test_create_item():
client = TestClient(app)
response = client.post(
"/items",
json={"name": "笔记本", "price": 25.0},
)
assert response.status_code == 200
data = response.json()
assert data["price_with_tax"] == 28.25

测试查询参数与请求头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@app.get("/search")
async def search(q: str, page: int = 1):
return {"query": q, "page": page}


@app.get("/protected")
async def protected(authorization: str = Header()):
return {"token": authorization}


def test_query_params():
client = TestClient(app)
# params 自动编码为 URL 查询字符串
response = client.get("/search", params={"q": "fastapi", "page": 2})
assert response.json() == {"query": "fastapi", "page": 2}


def test_headers():
client = TestClient(app)
response = client.get("/protected", headers={"Authorization": "Bearer xyz"})
assert response.json() == {"token": "Bearer xyz"}

使用 pytest fixture 消除重复

多次创建 TestClient 既啰嗦又浪费。用 pytest 的 fixture 管理它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pytest
from fastapi.testclient import TestClient
from main import app


@pytest.fixture
def client():
return TestClient(app)


def test_hello(client):
response = client.get("/hello")
assert response.status_code == 200


def test_create_item(client):
response = client.post("/items", json={"name": "笔", "price": 10.0})
assert response.status_code == 200

每个测试函数都会获得一个干净的 client 实例,互不干扰。

依赖覆写——测试的核心武器

实际项目中,路由往往依赖数据库连接、外部服务、认证逻辑。TestClient 配合 app.dependency_overrides 可以在不修改源代码的情况下替换依赖,实现真正的隔离测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
# main.py
from fastapi import Depends


def get_db():
"""生产环境:连接真实数据库"""
return RealDatabase()


@app.get("/items/{item_id}")
async def read_item(item_id: int, db=Depends(get_db)):
item = db.find(item_id)
return {"item": item, "id": item_id}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# test_main.py
from fastapi.testclient import TestClient
from main import app, get_db


class FakeDB:
"""内存假数据库,用于测试"""

def __init__(self):
self.data = {1: "苹果", 2: "香蕉"}

def find(self, item_id: int):
return self.data.get(item_id, "未知")


def fake_get_db():
return FakeDB()


def test_read_item():
# 覆写依赖
app.dependency_overrides[get_db] = fake_get_db
client = TestClient(app)

response = client.get("/items/1")
assert response.status_code == 200
assert response.json()["item"] == "苹果"

response = client.get("/items/999")
assert response.json()["item"] == "未知"

# 测试结束后清理
app.dependency_overrides.clear()

dependency_overrides 是一个字典:key 是原始依赖函数,value 是替换函数。FastAPI 在解析依赖时会优先检查这个字典。

更优雅的方式是用 fixture 管理覆写的生命周期:

1
2
3
4
5
@pytest.fixture
def client():
app.dependency_overrides[get_db] = fake_get_db
yield TestClient(app)
app.dependency_overrides.clear() # 测试后自动恢复

这样每个测试的覆写互不泄露,无需手动 clear()

测试认证接口

测试需要认证的接口时,通常覆写认证依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from fastapi.security import HTTPBearer

security = HTTPBearer()


def verify_token(credentials=Depends(security)):
token = credentials.credentials
if token != "secret-token":
raise HTTPException(status_code=401)
return {"user_id": 1}


@app.get("/me")
async def get_me(user=Depends(verify_token)):
return {"user_id": user["user_id"]}


def test_authenticated():
def fake_verify():
return {"user_id": 999}

app.dependency_overrides[verify_token] = fake_verify
client = TestClient(app)

response = client.get("/me")
assert response.status_code == 200
assert response.json()["user_id"] == 999
app.dependency_overrides.clear()

测试错误响应

错误路径的测试同样重要——确认接口在非法输入下返回正确的状态码和错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.get("/users/{user_id}")
async def get_user(user_id: int):
if user_id <= 0:
raise HTTPException(status_code=422, detail="用户 ID 必须为正整数")
return {"user_id": user_id}


def test_invalid_user_id(client):
response = client.get("/users/0")
assert response.status_code == 422
assert "用户 ID 必须为正整数" in response.json()["detail"]

response = client.get("/users/abc") # 类型不匹配
assert response.status_code == 422 # Pydantic 自动校验

FastAPI 的 Pydantic 校验会自动返回 422,你的自定义 HTTPException 可以覆盖任何状态码。

测试异步与后台任务

TestClientasync def 路由是透明的——你无需在测试中使用 async/await,客户端自动处理事件循环。

如果需要验证后台任务是否被注册,可以通过覆写 BackgroundTasks 依赖或检查任务的副作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from fastapi import BackgroundTasks

task_log = []


def bg_task(message: str):
task_log.append(message)


@app.post("/register")
async def register(background_tasks: BackgroundTasks):
background_tasks.add_task(bg_task, "注册完成")
return {"status": "ok"}


def test_background_task(client):
task_log.clear()
response = client.post("/register")
assert response.status_code == 200
# 注意:同步 TestClient 下后台任务会立即执行
assert "注册完成" in task_log

TestClient 的同步模式下,后台任务会在响应返回后、测试函数继续执行前完成(因为事件循环在同一个线程中被推进)。这使得验证后台任务的行为变得非常直接。

完整项目结构示例

推荐的测试组织方式:

1
2
3
4
5
6
7
8
project/
├── main.py # FastAPI 应用
├── dependencies.py # 所有可覆写的依赖
├── models.py # Pydantic 模型
├── tests/
│ ├── conftest.py # 全局 fixture
│ ├── test_items.py # 按模块拆分测试
│ └── test_users.py

conftest.py 统一管理 client fixture:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from main import app
from dependencies import get_db


class FakeDB:
def __init__(self):
self.data = {}

def find(self, item_id: int):
return self.data.get(item_id)


def fake_get_db():
return FakeDB()


@pytest.fixture
def client():
app.dependency_overrides[get_db] = fake_get_db
with TestClient(app) as client:
yield client
app.dependency_overrides.clear()

使用 with TestClient(app) as client 可以利用 httpx 的上下文管理器(自动管理连接),并且 yield 确保测试结束后清理覆写。

常见错误与排查

症状 原因 解决
AttributeError: 'async_generator' 依赖是 async generator,未正确覆写 覆写函数也返回 async generator,或用 contextmanager 包装
RuntimeError: Event loop is closed 测试中手动调用了 asyncio.run() 让 TestClient 自己管理事件循环,不要在测试中新建
覆写不生效 覆写的 key 和路由里不是同一个函数对象 确保 dep.overrides[key]keyDepends(func) 中的 func同一个对象
pytest 收集不到测试 测试文件命名不规范 文件名必须以 test_ 开头或结尾

小结

FastAPI 的 TestClient 让接口测试变得像调用 Python 函数一样简单:

  1. 创建 TestClient(app),无需启动真实服务器;
  2. client.get/post/put/delete 发送请求,response.json() 获取响应;
  3. 借助 app.dependency_overrides 替换真实依赖,实现隔离测试;
  4. pytest fixture 组织 client 和覆写,保持测试整洁;
  5. 错误路径也要覆盖——422、401、404 等状态码断言是信心的来源。

测试不是负担,而是重构的底气。用 TestClient 为你的 FastAPI 项目构建一套可重复、可信赖的回归测试,让每一次部署都安心。