测试 pytest 与 TestClient
本教程共 50 篇 · 第 46 篇 · 更新于 2026-08-12 · 约 8 分钟阅读
本节目标:学会用 pytest 加 TestClient 给 FastAPI 接口写自动化测试,发请求并断言结果,把测试从业务代码里独立出来。
代码写完不测,上线就心虚。FastAPI 基于 Starlette,自带一个 TestClient,配合成熟的 pytest 测试框架,写测试非常顺手。它底层用的是 httpx,接口风格和 requests 很像,老手一看就懂。
46-1 先装两个包
TestClient 需要 httpx 支撑,测试本身需要 pytest:
pip install httpx pytest
Note你也可以写成
from starlette.testclient import TestClient。FastAPI 直接把 Starlette 的这个类重新导出为fastapi.testclient.TestClient,二者等价,用哪个都行。
46-2 最小可跑的测试
假设我们有这样一个应用 main.py:
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"msg": "hello"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
写测试时,把 app 传给 TestClient,然后用 test_ 开头的函数写用例,用普通 assert 断言:
# test_main.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "hello"}
client.get("/") 就像用浏览器访问那个地址。response.status_code 是 HTTP 状态码,response.json() 把返回的 JSON 解析成 Python 字典,再和标准值比较。
Tip测试函数用普通
def,不要async def;调用client也是普通调用,不用await。这样能直接用pytest,不引入额外复杂度。只有调用你自己的异步函数时才需要异步测试(见官方进阶的 Async Tests)。
46-3 把测试放到独立的文件
真实项目里,测试应和业务代码分开。沿用上一章的多文件结构,把测试放进同一个包:
.
├── app
│ ├── __init__.py
│ ├── main.py
│ └── test_main.py
用相对导入从 main 模块拿到 app:
# app/test_main.py
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
46-4 用 @pytest.fixture 复用客户端
如果每个测试都新建 TestClient,既啰嗦又慢。用 pytest 的 fixture 可以创建一个共享的客户端,每个测试直接当参数用:
# app/test_main.py
import pytest
from fastapi.testclient import TestClient
from .main import app
@pytest.fixture
def client():
return TestClient(app)
def test_read_root(client):
response = client.get("/")
assert response.status_code == 200
def test_read_item(client):
response = client.get("/items/5?q=hi")
assert response.status_code == 200
assert response.json() == {"item_id": 5, "q": "hi"}
@pytest.fixture 把 client 变成可复用资源。测试函数把 client 写进参数,pytest 会自动注入。TestClient 支持 with 上下文管理,需要在测试前后做清理时这样写:
@pytest.fixture
def client():
with TestClient(app) as c:
yield c
yield c 之前的代码在测试前跑,之后的代码在测试后跑,适合做资源初始化和回收。
46-5 测试 POST 与其它请求
TestClient 支持 get、post、put、delete 等方法。POST 带 JSON 体时,用 json= 参数传一个 Python 字典:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.post("/items/")
async def create_item(item: Item):
return {"name": item.name, "price": item.price}
测试它的 POST:
def test_create_item(client):
payload = {"name": "苹果", "price": 3.5}
response = client.post("/items/", json=payload)
assert response.status_code == 200
assert response.json() == {"name": "苹果", "price": 3.5}
46-6 传头、查询、表单与 cookie
需要带请求信息时,参考 httpx 的写法即可:
- 路径参数、查询参数:直接拼进 URL,比如
client.get("/items/5?q=hi") - JSON 体:用
json=传字典 - 表单数据:用
data=而不是json= - 请求头:用
headers={"X-Token": "abc"}传字典 - Cookie:用
cookies={"session": "xyz"}传字典
def test_with_header(client):
# 假设应用里有一个需要 X-Token 头的 /secret 接口
response = client.get("/secret", headers={"X-Token": "fake-token"})
assert response.status_code == 200
NoteTestClient 接收的是「能被转成 JSON 的数据」,不是 Pydantic 模型对象。如果你在测试里有一个 Pydantic 模型,想把它转成可发送的数据,用
model_dump()再传给json=,例如client.post("/items/", json=item.model_dump())。
46-7 断言错误响应
接口可能在某些输入下返回 4xx。测试要覆盖这些情况:
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 0:
raise HTTPException(status_code=404, detail="找不到")
return {"item_id": item_id}
def test_item_not_found(client):
response = client.get("/items/0")
assert response.status_code == 404
assert response.json()["detail"] == "找不到"
46-8 运行测试
在项目根目录执行:
pytest
pytest 会自动发现 test_*.py 文件和里面 test_ 开头的函数,逐个运行并汇报结果。看到绿色的 passed 就说明通过了。加 -v 看每个用例详情:
pytest -v
Tip给某个接口加依赖、改了逻辑后,先跑一遍
pytest,能立刻知道有没有改坏别的地方。把测试当成一个永远在线的回归保护网。
46-9 组织多个测试文件
项目变大后,测试会分成多个文件。约定俗成用 tests/ 目录,里面放 test_xxx.py。pytest 会递归找到它们。如果多个文件都要同一个客户端,把 fixture 放进 tests/conftest.py,pytest 会自动在所有测试里提供它,不用每个文件都 import。
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture
def client():
return TestClient(app)
# tests/test_items.py
def test_read_item(client):
r = client.get("/items/5")
assert r.status_code == 200
只想跑某个文件或某个用例时,把路径或 -k 关键字传给 pytest:
pytest tests/test_items.py
pytest -k "item"
-k 会挑出名字里含 item 的测试,调试单个功能时很有用。另外两个常用开关也建议记住:-x 遇到第一个失败就停下,适合在修 bug 时快速定位;-q 精简输出,用例多的时候屏幕会干净很多。
还有个容易忽略的细节:每个测试用例都应当能独立运行,不依赖其它用例的执行顺序。上面的 client fixture 每次调用都新建一个客户端,就是在贯彻这个原则。一旦用例之间靠共享状态传递数据,单跑某一条就会失败,测试也就失去了排查问题的价值。
46-10 异步代码的测试要点
路径操作常写成 async def,但 TestClient 的调用是同步的,这没问题——TestClient 内部会处理好事件循环。只有当你的测试里要直接调用「自己的异步函数」(比如异步查数据库函数)时,才需要异步测试设施。
这时可以给测试函数加 @pytest.mark.anyio 之类标记,或用 Starlette 的异步测试支持。大多数写接口测试的场景,用同步的 client.get/post 加普通 assert 就够了,不必把测试函数写成 async def。
Note记住一个原则:测试「接口」用同步 TestClient;测试「内部异步工具函数」才需要异步测试。本册聚焦接口测试,这层已经能覆盖绝大多数回归需求。
46-11 小结
测试三件套:装 httpx 和 pytest;用 TestClient(app) 发请求;用 assert 检查状态码和 JSON。把客户端做成 fixture 复用,POST 用 json= 传体,带头和 cookie 用对应字典参数。最后 pytest 一键运行。
测试不是上线前的负担,而是省时间的投资。一次写好,之后每次改代码都能自动帮你拦住回归错误。
Tip把测试养成习惯:每加一个接口,顺手补一两条用例,覆盖正常返回和典型错误(如 404、422)。积累起来,重构代码时就有了安全网,敢改、改得快。测试用例本身也是给同事看的「接口用法说明书」。