首页 / FastAPI 入门教程 / 测试 pytest 与 TestClient

FastAPI 入门教程

测试 pytest 与 TestClient

本教程共 50 篇 · 第 46 篇 · 更新于 2026-08-12 · 约 8 分钟阅读

FastAPIFastAPI 入门教程测试pytestTestClienthttpx

本节目标:学会用 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.fixtureclient 变成可复用资源。测试函数把 client 写进参数,pytest 会自动注入。TestClient 支持 with 上下文管理,需要在测试前后做清理时这样写:

@pytest.fixture
def client():
    with TestClient(app) as c:
        yield c

yield c 之前的代码在测试前跑,之后的代码在测试后跑,适合做资源初始化和回收。

46-5 测试 POST 与其它请求

TestClient 支持 getpostputdelete 等方法。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}

需要带请求信息时,参考 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
Note

TestClient 接收的是「能被转成 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 小结

测试三件套:装 httpxpytest;用 TestClient(app) 发请求;用 assert 检查状态码和 JSON。把客户端做成 fixture 复用,POST 用 json= 传体,带头和 cookie 用对应字典参数。最后 pytest 一键运行。

测试不是上线前的负担,而是省时间的投资。一次写好,之后每次改代码都能自动帮你拦住回归错误。

Tip

把测试养成习惯:每加一个接口,顺手补一两条用例,覆盖正常返回和典型错误(如 404、422)。积累起来,重构代码时就有了安全网,敢改、改得快。测试用例本身也是给同事看的「接口用法说明书」。