代码语言

知识点思维导图

29 个知识节点

Python(31) - 测试

读完后,你应能完成以下任务:

  • 绘制“Python(31) - 测试 / 先建立前端锚点”的关键对象与数据流,解释“pytest 和 Jest/Vitest 是同一类东西——自动发现测试文件、跑用例、报告结果。”,并用源码位置、日志或 Trace 标注证据。
  • 为“Python(31) - 测试 / fixture:比 beforeEach 更聪明的"按需注入"”设计正常与异常输入,验证“前端的 beforeEach 是"每个用例前都跑一遍准备逻辑"。”,输出首个偏差位置与回归测试结果。
  • 实现“Python(31) - 测试 / setup / teardown:用 yield 写"前置 + 收尾"”的最小代码或配置,检验“需要"用完要清理"(关数据库、删临时文件)时,用 yield——yield 之前是 setup,之后是 teardown。”,输出命令、结果与 Diff,并说明不适用边界。

你在前端写过 Jest/Vitest:test() 包一个用例、expect().toBe() 断言、beforeEach 准备数据、test.each 跑参数化、jest.mock 打桩——那 Python 的 pytest 你已经会了一大半,换的只是语法。本篇解决四个问题:pytest 怎么写一个用例(先用 Jest 锚直觉)、fixture 怎么替代 beforeEach、参数化和 mock 怎么写、以及怎么测一个 FastAPI 接口。顺带划清几处和 Jest 不一样的坑(断言不是链式 API、fixture 比 beforeEach 更"按需注入")。

一、先建立前端锚点

pytest 和 Jest/Vitest 是同一类东西——自动发现测试文件、跑用例、报告结果。心智几乎可以平移:

// Jest / Vitest:describe 分组,test 一个用例,expect 链式断言
test('add 两数相加', () => {
  expect(add(2, 3)).toBe(5)
})

最大的体感差异一眼可见:pytest 没有 expect().toBe() 这套链式断言,直接用语言原生的 assert。断言失败时 pytest 会智能地把 2 + 3 实际算出的值、期望值都打印出来,所以你不会因为"裸 assert"而丢失信息。

Jest / Vitest pytest 说明
test('xx', fn) def test_xx(): 一个用例
describe('组', ...) class TestXxx: 或按文件分 分组
expect(a).toBe(b) assert a == b 断言(原生关键字)
expect(fn).toThrow() with pytest.raises(...) 断言抛异常
beforeEach(fn) @pytest.fixture 准备数据/环境
test.each([...]) @pytest.mark.parametrize 参数化
jest.mock(...) monkeypatch / unittest.mock 打桩
jest --coverage pytest --cov 覆盖率

一句话切入点:pytest = Jest 的心智 + 用原生 assert 替代 expect + 用 fixture 替代 beforeEach


二、装与跑:约定大于配置

pytest 是第三方库,不在标准库里,先装(参考第 12 篇虚拟环境与依赖管理):

pip install pytest            # 安装 pytest 本体
pytest                        # 在项目根目录直接跑,它会自动发现所有测试
pytest -v                     # -v:verbose,逐条打印用例名和结果
pytest tests/test_math.py     # 只跑某个文件
pytest -k "add"               # 只跑名字里含 add 的用例(类似 jest -t)

pytest 靠命名约定自动发现测试,不用像 Jest 那样配 testMatch

约定项 规则
测试文件 test_*.py*_test.py
测试函数 test_ 开头
测试类 Test 开头,且不能写 __init__

放一个完整最小例子。被测代码 calc.py

测试文件 test_calc.py

// Jest 等价写法对照
test('divide by zero throws', () => {
  expect(() => divide(10, 0)).toThrow()
})

注意 pytest.raises 用的是 with 上下文管理器(第 09 篇讲过),把"期望抛错的代码"放进 with 块里,比 Jest 包一层箭头函数更直观。


三、fixture:比 beforeEach 更聪明的"按需注入"

前端的 beforeEach 是"每个用例前都跑一遍准备逻辑"。pytest 的 fixture 思路升级了一层:你声明一个 fixture,哪个用例需要它,就把 fixture 名字写进该用例的参数里——pytest 看到参数名就自动注入。这其实就是第 18 篇讲过的依赖注入思想。

// Jest 对照:beforeEach 把数据塞到外层变量,所有用例共享
let sampleUser
beforeEach(() => { sampleUser = { id: 1, name: 'tom' } })
test('user name', () => { expect(sampleUser.name).toBe('tom') })

关键差异(边界):

  • Jest 的 beforeEach 是"无差别地每个用例前都跑";pytest 的 fixture 是**"用例显式写了参数才注入,没写就不跑"**——更精准、更省。
  • fixture 可以互相依赖:一个 fixture 的参数里又可以写另一个 fixture 名,pytest 自动级联解析。这点 beforeEach 做不到。

3.1 setup / teardown:用 yield 写"前置 + 收尾"

需要"用完要清理"(关数据库、删临时文件)时,用 yield——yield 之前是 setup,之后是 teardown。这正好对标 Jest 的 beforeEach + afterEach 合体:

这里的 yield 是第 06 篇生成器的同款语法:函数执行到 yield 暂停、把值交出去,用例跑完再回来执行后面的清理。pytest 巧妙地借生成器实现了"前后夹逻辑"。

3.2 scope:控制 fixture 多久重建一次

fixture 默认每个用例都重新执行一次scope="function")。如果准备成本高(比如起一个测试数据库),可以放宽作用域:

scope 重建频率 类比
function(默认) 每个用例一次 beforeEach
module 每个文件一次 文件级 beforeAll
session 整个 pytest 进程一次 全局 beforeAll

3.3 conftest.py:放公共 fixture 的地方

跨多个测试文件复用的 fixture,放进一个叫 conftest.py 的文件里,pytest 会自动加载、自动共享,不用 import。它类似 Jest 的 setup.js / 全局 jest.config 里的公共配置。

tests/
├── conftest.py        # 公共 fixture 放这里,同目录所有 test_*.py 自动可用
├── test_user.py
└── test_order.py

四、参数化:一份逻辑,多组数据

前端用 test.each 跑"同一逻辑、多组输入输出"。pytest 用 @pytest.mark.parametrize 装饰器(装饰器见第 10 篇):

// Jest 对照:test.each
test.each([
  [2, 3, 5],
  [-1, 1, 0],
  [0, 0, 0],
])('add(%i, %i) = %i', (a, b, expected) => {
  expect(add(a, b)).toBe(expected)
})

跑起来你会看到三个独立用例(不是一个),哪组挂了报告会精确指出是哪组数据——这比手写三个 assert 强在定位。


五、mock:隔离外部依赖

测试时不想真的发网络请求、真的查数据库,就要"打桩"。Python 有两种常见做法。

5.1 方式一:monkeypatch(pytest 内置 fixture,最轻量)

monkeypatch 是 pytest 自带的 fixture,用来临时替换对象的属性/方法,用例结束自动还原

// Jest 对照:jest.spyOn 临时替换实现,afterEach 自动还原
jest.spyOn(axios, 'get').mockResolvedValue({ data: { name: 'mock_tom' } })

5.2 方式二:unittest.mock(标准库,功能更全)

需要"断言某函数被调用了几次、用什么参数调用"时,用标准库 unittest.mockMock / patch

⚠️ 最易踩的坑:patch 的路径要写"使用处"而不是"定义处"。如果 calc.pyimport requests 后用 requests.get,就 patch "calc.requests.get",而不是 "requests.get"。这和 JS 里 mock 模块按引用路径生效是同一类陷阱。


六、测一个 FastAPI 接口

前面是纯函数测试。后端真正高频的是测接口。FastAPI(第 14 篇)配套提供了 TestClient,用法几乎和前端用 supertest 测 Express 一样——不用真起服务器,直接在内存里发请求。

// Express + supertest 对照:思路一模一样
const res = await request(app).get('/items/42')
expect(res.status).toBe(200)
expect(res.body).toEqual({ item_id: 42, name: 'item-42' })

TestClient 底层基于 httpx,能同步调用异步接口,所以即便你的 read_item 写成 async def(异步见第 17 篇),测试代码这里也无需写 await,照样同步调用——这是它帮你抹平的细节。

测带参数校验的接口时,顺手验证"非法输入会被 Pydantic 拦下"(数据校验见第 15 篇):


七、覆盖率与常用命令速查

覆盖率告诉你"代码有多少被测试跑到了",装 pytest-cov 插件:

pip install pytest-cov            # 覆盖率插件
pytest --cov=calc                 # 统计 calc 模块的覆盖率
pytest --cov=. --cov-report=html  # 生成 HTML 报告,浏览器打开看红绿

高频命令汇总:

命令 作用 Jest 类比
pytest 跑全部测试 jest
pytest -v 显示每条用例名 jest --verbose
pytest -k "add" 按名字筛选用例 jest -t add
pytest -x 一旦失败立即停止 jest --bail
pytest --lf 只重跑上次失败的(last-failed) jest --onlyFailures
pytest -s 不捕获输出,让 print 显示出来
pytest --cov=. 覆盖率 jest --coverage

八、总结

  • 先建立前端锚点:pytest 和 Jest/Vitest 是同一类东西——自动发现测试文件、跑用例、报告结果。
  • 装与跑:约定大于配置:| 测试类 | 以 Test 开头,且不能写 init |
  • fixture:比 beforeEach 更聪明的"按需注入":前端的 beforeEach 是"每个用例前都跑一遍准备逻辑"。
  • 参数化:一份逻辑,多组数据:前端用 test.each 跑"同一逻辑、多组输入输出"。
  • mock:隔离外部依赖:⚠️ 最易踩的坑:patch 的路径要写"使用处"而不是"定义处"。
  • 覆盖率与常用命令速查:| pytest -x | 一旦失败立即停止 | jest --bail |

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“测试”中,需要同时满足“先建立前端锚点”与“装与跑:约定大于配置”。给定正文约束“断言失败时 pytest 会智能地把 2 + 3 实际算出的值、期望值都打印出来,所以你不会因为"裸 assert"而丢失信息。”,哪些判断保持了原有处理机制?多选
2“测试”出现偏差:“在“测试 / fixture:比 beforeEach 更聪明的"按需注入"”中,即使不满足“你声明一个 fixture,哪个用例需要它,就把 fixture 名字写进该用例的参数里——pytest 看到参数名就自动注入”,结果与副作用仍会保持不变。”已成为实际行为。围绕“fixture:比 beforeEach 更聪明的"按需注入"”与“setup / teardown:用 yield 写"前置 + 收尾"”,哪些判断能定位被改变的职责或边界?多选
3评审“测试”方案时,验收条件包含“fixture 默认每个用例都重新执行一次(scope="function")。”。关于“scope:控制 fixture 多久重建一次”与“conftest.py:放公共 fixture 的地方”的哪些决策符合正文机制?多选