FastAPI 应用测试入门:用 TestClient 与 pytest 编写首个 API 测试 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 风格十分熟悉、直观。你可以直接把pytest与FastAPI搭配使用编写普通def测试函数、发起同步请求、用assert做断言无需任何特殊框架适配。阅读本文后你将掌握搭建测试环境、使用TestClient对路径操作发起 GET/POST 请求并断言状态码与 JSON 响应、把测试拆分为独立文件融入真实项目结构以及使用 pytest 一键运行全部测试的完整流程。本文以官方教程文档 docs/en/docs/tutorial/testing.md 为骨架展开并补充了 FastAPI 仓库中对应的可运行示例源码与仓库自带的测试用例方便你对照验证。为什么可以用 pytest 直接测试 FastAPIFastAPI 测试能力的基石是 Starlette 的TestClient。在 fastapi/testclient.py 中可以看到它只做了一件事——把 Starlette 的TestClient原样重新导出from starlette.testclient import TestClient as TestClient # noqa也就是说from fastapi.testclient import TestClient与from starlette.testclient import TestClient是同一个对象FastAPI 只是出于开发者便利将它再暴露一次。技术细节层面它仍然直接来自 Starlette。TestClient内部基于 HTTPX而 HTTPX 的设计又以 Requests 为蓝本所以三者共享高度一致的使用体验用client.get(...)、client.post(...)等发起请求通过response.status_code读取状态码通过response.json()读取解析后的 JSON 响应体通过response.text、response.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}要点拆解导入TestClient从fastapi.testclient导入。创建客户端把 FastAPI 应用实例传给TestClientclient TestClient(app)。TestClient会像真实服务器一样驱动 ASGI 应用处理请求。以test_前缀定义函数test_read_main是标准 pytest 约定会被自动收集为一条测试用例。像使用httpx一样调用 clientclient.get(/)直接返回响应对象。用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.pyfrom 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.pyfrom 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.pyfrom 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_modelItem) async def read_main(item_id: str, x_token: Annotated[str, Header()]): if x_token ! fake_secret_token: raise HTTPException(status_code400, detailInvalid X-Token header) if item_id not in fake_db: raise HTTPException(status_code404, detailItem 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_code400, detailInvalid X-Token header) if item.id in fake_db: raise HTTPException(status_code409, detailItem already exists) fake_db[item.id] item.model_dump() return item该应用覆盖了几种典型测试场景GET /items/{item_id}使用内存字典fake_db模拟数据库Token 无效返回 400条目不存在返回 404POST /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.pyfrom 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_py310与app_b_an_py310两个版本的test_main依次调用 6 个测试函数从项目自身测试体系中印证了示例的正确性。在测试请求中传递各类数据当你不确定如何通过 client 在请求中携带某种数据时可以先去检索httpx或requests因为二者设计同源的用法然后在测试里照做即可。常见映射如下路径或查询参数直接写进 URL例如client.get(/items/{item_id}?verbose1)或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 pytestpytest 会自动发现以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),仅供参考