FastAPI 应用测试入门:用 TestClient 与 pytest 编写首个 API 测试
2026/9/7 2:25:09 网站建设 项目流程

FastAPI 应用测试入门:用 TestClient 与 pytest 编写首个 API 测试

【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi

得益于 Starlette 提供的TestClient,为FastAPI应用编写测试变得轻松而愉快。它基于 [HTTPX] 实现,而 HTTPX 又是在 Requests 的基础上设计的,因此 API 风格十分熟悉、直观。你可以直接把pytestFastAPI搭配使用:编写普通def测试函数、发起同步请求、用assert做断言,无需任何特殊框架适配。阅读本文后,你将掌握:搭建测试环境、使用TestClient对路径操作发起 GET/POST 请求并断言状态码与 JSON 响应、把测试拆分为独立文件融入真实项目结构,以及使用 pytest 一键运行全部测试的完整流程。

本文以官方教程文档 docs/en/docs/tutorial/testing.md 为骨架展开,并补充了 FastAPI 仓库中对应的可运行示例源码与仓库自带的测试用例,方便你对照验证。

为什么可以用 pytest 直接测试 FastAPI

FastAPI 测试能力的基石是 Starlette 的TestClient。在 fastapi/testclient.py 中可以看到它只做了一件事——把 Starlette 的TestClient原样重新导出:

from starlette.testclient import TestClient as TestClient # noqa

也就是说,from fastapi.testclient import TestClientfrom starlette.testclient import TestClient是同一个对象,FastAPI 只是出于开发者便利将它再暴露一次。技术细节层面,它仍然直接来自 Starlette。

TestClient内部基于 HTTPX,而 HTTPX 的设计又以 Requests 为蓝本,所以三者共享高度一致的使用体验:

  • client.get(...)client.post(...)等发起请求;
  • 通过response.status_code读取状态码;
  • 通过response.json()读取解析后的 JSON 响应体;
  • 通过response.textresponse.headers等访问文本与头信息。

正因如此,你可以零成本地把测试逻辑直接交给 pytest:测试函数命名以test_开头即可被 pytest 自动发现。官方文档明确说明,测试函数应写成普通def(而非async def),对 client 的调用也是普通调用(不使用await),这样 pytest 无需任何插件即可直接运行。

快速上手:第一个 TestClient 测试

安装依赖

要使用TestClient,请先安装httpx

$ uv add httpx

仓库中对应的入门示例位于 docs_src/app_testing/tutorial001_py310.py,完整代码如下:

from fastapi import FastAPI from fastapi.testclient import TestClient app = FastAPI() @app.get("/") async def read_main(): return {"msg": "Hello World"} client = TestClient(app) def test_read_main(): response = client.get("/") assert response.status_code == 200 assert response.json() == {"msg": "Hello World"}

要点拆解:

  1. 导入TestClient:从fastapi.testclient导入。
  2. 创建客户端:把 FastAPI 应用实例传给TestClientclient = TestClient(app)TestClient会像真实服务器一样驱动 ASGI 应用处理请求。
  3. test_前缀定义函数test_read_main是标准 pytest 约定,会被自动收集为一条测试用例。
  4. 像使用httpx一样调用 clientclient.get("/")直接返回响应对象。
  5. assert做标准断言:既断言状态码为 200,也断言返回的 JSON 与预期完全一致。

这里需要留意:路径操作函数read_main本身是async def,但测试函数是普通def,调用也是同步的——TestClient在内部帮你完成了事件循环的编排。这也正是它能与 pytest 无缝配合的原因。仓库自带的回归测试 tests/test_tutorial/test_testing/test_tutorial001.py 不仅执行了这个示例测试函数,还会请求/openapi.json并断言自动生成的 OpenAPI 架构快照,说明「测试客户端运行应用 + 访问自动生成的接口文档」都是同一套机制可以覆盖的。

在真实项目中分离测试文件

真实应用很少只有单文件,测试通常放在独立文件中。官方文档延续了 Bigger Applications - 多文件应用 中介绍的项目结构来演示分层。

应用文件结构

假设你拥有如下结构:

. ├── app │ ├── __init__.py │ └── main.py

main.py中定义 FastAPI 应用,对应源码见 docs_src/app_testing/app_a_py310/main.py:

from fastapi import FastAPI app = FastAPI() @app.get("/") async def read_main(): return {"msg": "Hello World"}

在同一个包内放置测试文件

把测试文件test_main.py放进同一个 Python 包(即与main.py同目录且该目录含__init__.py),目录结构变为:

. ├── app │ ├── __init__.py │ ├── main.py │ └── test_main.py

因为测试文件与main.py处于同一包中,可以直接使用相对导入拿到app对象,对应源码见 docs_src/app_testing/app_a_py310/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 assert response.json() == {"msg": "Hello World"}

与入门示例相比,唯一的差别在于通过from .main import app导入应用;其余测试代码完全一致。仓库测试 tests/test_tutorial/test_testing/test_main_a.py 会在项目 CI 中实际导入docs_src.app_testing.app_a_py310.test_main并调用test_read_main()与对/openapi.json的断言,直接验证了这套示例的可运行性。

扩展实战:测试带认证与多状态码的接口

官方文档随后把示例升级为更贴近真实业务的场景:接口要求X-Token请求头,GET可能返回错误,POST可能返回多种错误。

扩展版应用

对应的main.py源码位于 docs_src/app_testing/app_b_an_py310/main.py:

from typing import Annotated from fastapi import FastAPI, Header, HTTPException from pydantic import BaseModel fake_secret_token = "coneofsilence" fake_db = { "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, } app = FastAPI() class Item(BaseModel): id: str title: str description: str | None = None @app.get("/items/{item_id}", response_model=Item) async def read_main(item_id: str, x_token: Annotated[str, Header()]): if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item_id not in fake_db: raise HTTPException(status_code=404, detail="Item not found") return fake_db[item_id] @app.post("/items/") async def create_item(item: Item, x_token: Annotated[str, Header()]) -> Item: if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: raise HTTPException(status_code=409, detail="Item already exists") fake_db[item.id] = item.model_dump() return item

该应用覆盖了几种典型测试场景:

  • GET /items/{item_id}:使用内存字典fake_db模拟数据库,Token 无效返回 400,条目不存在返回 404;
  • POST /items/:校验请求体(Pydantic 的Item模型)与X-Token,条目已存在时返回 409 冲突;
  • 两个接口都以Annotated[str, Header()]声明必需请求头X-Token,缺失或错误都会触发 400。

仓库中另有等价写法 docs_src/app_testing/app_b_py310/main.py,二者仅在类型标注风格上不同,便于你选择。

扩展版测试

对应的test_main.py源码位于 docs_src/app_testing/app_b_an_py310/test_main.py:

from fastapi.testclient import TestClient from .main import app client = TestClient(app) def test_read_item(): response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) assert response.status_code == 200 assert response.json() == { "id": "foo", "title": "Foo", "description": "There goes my hero", } def test_read_item_bad_token(): response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) assert response.status_code == 400 assert response.json() == {"detail": "Invalid X-Token header"} def test_read_nonexistent_item(): response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) assert response.status_code == 404 assert response.json() == {"detail": "Item not found"} def test_create_item(): response = client.post( "/items/", headers={"X-Token": "coneofsilence"}, json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, ) assert response.status_code == 200 assert response.json() == { "id": "foobar", "title": "Foo Bar", "description": "The Foo Barters", } def test_create_item_bad_token(): response = client.post( "/items/", headers={"X-Token": "hailhydra"}, json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, ) assert response.status_code == 400 assert response.json() == {"detail": "Invalid X-Token header"} def test_create_existing_item(): response = client.post( "/items/", headers={"X-Token": "coneofsilence"}, json={ "id": "foo", "title": "The Foo ID Stealers", "description": "There goes my stealer", }, ) assert response.status_code == 409 assert response.json() == {"detail": "Item already exists"}

这套测试逐一覆盖了「正确 Token + 正常读取」「错误 Token」「条目不存在」「创建新条目」「创建重复条目」等分支。仓库中的 tests/test_tutorial/test_testing/test_main_b.py 以 pytest fixture 参数化的方式同时导入了app_b_py310app_b_an_py310两个版本的test_main,依次调用 6 个测试函数,从项目自身测试体系中印证了示例的正确性。

在测试请求中传递各类数据

当你不确定如何通过 client 在请求中携带某种数据时,可以先去检索httpx(或requests,因为二者设计同源)的用法,然后在测试里照做即可。常见映射如下:

  • 路径或查询参数:直接写进 URL,例如client.get("/items/{item_id}?verbose=1")client.get("/items/foo")
  • JSON 请求体:把一个 Python 对象(如dict)传给json参数,例如client.post("/items/", json={"id": "foobar"})
  • 表单数据(Form Data):改用data参数传dict,例如client.post("/login", data={"username": "johndoe"})
  • 请求头:以dict传给headers参数,例如示例中的headers={"X-Token": "coneofsilence"}
  • Cookie:以dict传给cookies参数。

一个容易踩坑的点是:TestClient接收的是可以被 JSON 序列化的数据,而不是 Pydantic 模型本身。如果测试中持有 Pydantic 模型并希望以 JSON 形式发给应用,应先用 JSON 兼容编码器教程 中介绍的jsonable_encoder转换后再传给json参数。

运行测试

先安装 pytest:

$ uv add pytest

随后在项目根目录直接运行:

$ uv run pytest

pytest 会自动发现以test_开头的文件与函数,逐个执行并汇总报告:

$ uv run pytest ================ test session starts ================ platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 rootdir: /home/user/code/superawesome-cli/app plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 collected 6 items ---> 100% test_main.py ...... [100%] ================= 1 passed in 0.03s =================

6 个测试用例全部通过。FastAPI 仓库自身也正是用这一模式来守护行为:例如 tests/test_tutorial/test_testing/test_main_b.py 等文件直接 importdocs_src下的示例并执行其中的测试函数,让教程代码始终与框架实现保持同步、可运行。

延伸阅读

  • 若你想在测试中调用除发送请求之外的async函数(例如异步数据库操作),可以参考进阶教程中的 异步测试(Async Tests);
  • 多文件应用的组织方式见 Bigger Applications - 多文件应用;
  • 有关 Pydantic 模型在测试中序列化的问题,参见 JSON 兼容编码器(JSON Compatible Encoder);
  • 仓库还提供了异步测试示例源码 docs_src/async_tests 与依赖注入场景的测试示例 docs_src/dependency_testing,可作为进阶参考。

【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询