文件名 test_.py、函数名 test_,两个约定缺一个它就不跑。 断言就是 Python 自带的 assert,pytest 的活儿是把失败现场摆给你看。 它不给你 assert 5 == 6 就算完,还补一句 where 5 = add(2, 3)。
这一篇对应教材 11.1 到 11.1.5:尝试使用 pytest、安装、回归测试、执行测试、 确认未通过时的运作情况、复原一个测试。 第 10 章把错误页做完了,这一篇开始「让机器替我记得」。
一、装与不装
pip install pytest
pytest -v # 详细模式
python -m pytest -q # 简洁模式
本沙箱已装 pytest 9.2.0,下面的输出都是真跑出来的。
pytest 和 python -m pytest 有一点差别:后者会把当前目录加进 sys.path,
所以 import web 能成。这个差别在导入报错的时候很关键。
起手可以直接 python -m pytest,省一类莫名其妙的 ModuleNotFoundError。
二、它认什么样子的测试
# tests/test_calc.py
def add(a, b): # calc.py 里就这么一个函数
return a + b
def test_add():
assert add(2, 3) == 5
两个约定:文件名 test_*.py、函数名 test_*。都满足才会被收集。
实测 collected 3 items,三条各自 PASSED。
第一条测试写完最常见的问题就是这个:文件名写成 testcalc.py 或者函数叫 check_add,
pytest 一声不吭地跳过,你会以为「测试通过了」。
它就是「一条都不跑」也会显示 0 passed,不会报错。所以第一次跑的时候,先数一下 collected 几个。
三、失败了它给你看什么
把断言改成 == 6,实测输出是这样:
| 行 | 内容 |
|---|---|
| 源码 | > assert add(2, 3) == 6 |
| 结论 | E assert 5 == 6 |
| 来源 | E + where 5 = add(2, 3) |
| 位置 | tests\test_calc.py:5: AssertionError |
这就是 pytest 最值钱的地方:它把 add(2, 3) 真的算给你看了。
不用写 print 去猜、不用单步调试。失败现场自己摆出来,还告诉你「5 是从哪来的」。
对比一下 unittest 的 assertEqual:它只告诉你 AssertionError: 5 != 6,
不告诉你是哪个表达式算出来的。表达式简单的时候无所谓,嵌套三层函数调用的时候就是灾难。
四、一条挂了,其余还跑不跑
实测三条测试里,一条断言错、一条直接抛 ZeroDivisionError:汇总行是 2 failed, 1 passed。
第三条照样跑了。这是默认行为,也是你想要的——一次看到所有问题,不用修一个跑一次。
加 -x 之后只跑到第一个错就停,汇总行是 1 failed。
调试的时候用 -x,改一个看一个;提交前跑全量,看有没有连带坏掉的。
返回码也是重点:通过是 0,有失败是 1。CI 就靠这个数字判断该不该拦下提交。
五、修好一条,留一条错的
实测汇总行 1 failed, 2 passed。
这一行是这一章的主界面。测试写多了以后,习惯会变成「先看这行,绿的就提交」。
顺带说一个容易误会的地方:1 failed, 2 passed 里那个 passed 是「跑过了」,不是「功能对了」。
测试本身写错了也会 pass——比如断言写成 assert add(2, 3),非零就通过,等于什么都没测。
六、一条测试该写多长
| 该测 | 不该测 |
|---|---|
| 一个行为,一条测试 | 一条测试里跑十个场景 |
| 函数名说清「测什么」 | test1、test2、test3 |
| 测输入与输出的关系 | 测内部用了哪个函数 |
| 边界值单独一条 | 把边界塞进正常路径里 |
右边那列的后果是一样的:失败时你不知道坏在哪。一条测试断言了五件事,红了以后还得挨个查。
函数名那条尤其重要:
test_delete_other_users_image_returns_403 比 test_delete2 有用得多——跑挂了的时候,
报告里那行字就是结论。
第 8 章那七个端点,每个都能拆出三到五条测试。
写不写全看项目大小,但「权限」和「找不到」这两类必须写,它们是最容易回归的地方。
七、动手 Demo:pytest 入门:一句断言与它失败时的样子
这个 Demo 从三条测试开始,然后一条一条改坏,看 pytest 报什么。 第二节那三行 E 开头的输出,是这一篇最该带走的东西。
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 6
def test_zero_div():
assert 1 / 0 == 0
实跑输出:
--------------------------------------------------------------------
pytest 入门:一句断言与它失败时的样子
--------------------------------------------------------------------
① 三条测试,跑一遍
--------------------------------------------------------------------
返回码 -> 0
============================= test session starts =============================
platform win32 -- Python 3.13.14, pytest-9.2.0, pluggy-1.7.0
collecting ... collected 3 items
tests/test_calc.py::test_add PASSED [ 33%]
tests/test_calc.py::test_add_zero PASSED [ 66%]
tests/test_calc.py::test_add_negative PASSED [100%]
============================== 3 passed in 0.09s ==============================
! test_ 开头的函数才算测试,文件名也得是 test_*.py。
这两个条件是 pytest 的约定,不满足的话它根本不会去跑——第一条测试写完最常见的问题就是这个。
! assert 就是 Python 自带的断言,不用记任何新语法。
pytest 的活儿是「把每一条 assert 失败时的现场保留下来」。
② 故意写错一条
--------------------------------------------------------------------
返回码 -> 1
F. [100%]
================================== FAILURES ===================================
__________________________________ test_add ___________________________________
def test_add():
> assert add(2, 3) == 6
E assert 5 == 6
E + where 5 = add(2, 3)
tests\test_calc.py:5: AssertionError
=========================== short test summary info ===========================
FAILED tests/test_calc.py::test_add - assert 5 == 6
1 failed, 1 passed in 0.16s
! 看那三行 E 开头的:它把 add(2, 3) 真的算给你看了——assert 5 == 6,
再补一句 where 5 = add(2, 3)。
! 这就是 pytest 最值钱的地方:不用写 print 去猜,失败现场自己摆出来。
unittest 的 assertEqual 只告诉你「不相等」,不给值。
③ 一条失败,其余还跑不跑
--------------------------------------------------------------------
返回码 -> 1
E assert 5 == 6
E + where 5 = add(2, 3)
E ZeroDivisionError: division by zero
calc.py:6: ZeroDivisionError
FAILED tests/test_calc.py::test_zero_div - ZeroDivisionError: division by zero
2 failed, 1 passed in 0.17s
! 默认会跑完全部再汇总:一条断言错、一条直接抛异常,第三条照样跑了。
这正是你想要的——一次看到所有问题,不用修一个跑一次。
! 返回码非 0 这一点很关键:CI 靠它判断该不该拦下这次提交。
④ 只想让它停在第一个错
--------------------------------------------------------------------
加 -x 之后 -> 返回码 1
1 failed in 0.16s
! 只跑了第一条就停。调试的时候用 -x,改一个看一个;提交前跑全量,看有没有连带坏掉的。
⑤ 把测试修回来
--------------------------------------------------------------------
修好一条、留一条错的 -> 返回码 1
1 failed, 2 passed in 0.16s
! 汇总行 1 failed, 2 passed 是这一章的主界面。看这一行就知道整体状态。
! 测试写多了以后,习惯会变成「先看这行,绿的就提交」。
⑥ 一条测试该写多长
--------------------------------------------------------------------
| 该测 | 不该测 |
|---|---|
| 一个行为,一条测试 | 一条测试里跑十个场景 |
| 函数名说清「测什么」 | test1、test2、test3 |
| 测输入与输出的关系 | 测内部用了哪个函数 |
| 边界值单独一条 | 把边界塞进正常路径里 |
! 右边那列的后果是一样的:失败时你不知道坏在哪。一条测试断言了五件事,红了以后还得挨个查。
! 函数名那条尤其重要:
test_delete_other_users_image_returns_403 比 test_delete2 有用得多——跑挂了的时候,
报告里那行字就是结论。
第一节 collected 3 items 这个数要养成习惯看一眼。
第二节那三行 E:assert 5 == 6,下一行还说 5 是 add(2, 3) 算出来的。
第三、四节的汇总行对比:默认全跑完,加 -x 只跑第一个。
30 秒实验
- 把测试文件名改成
calc_test.py,再跑一次,看它收集到几个。 - 把一条测试的断言改成
assert add(2, 3),看它还会不会失败。 - 想一想:为什么返回码非 0 这件事很重要?
常见误区
- 文件名或函数名不带 test_:一条都不跑,还显示 0 passed。
- 以为 passed 就代表功能对:测试写错了也会通过。
- 一条测试断言五件事:挂了不知道坏在哪。
- 只看输出不看返回码:CI 靠返回码判断,手工也应该看。
自测三问(点开对答案)
1. pytest 认哪两个命名约定? —— 文件名 test_*.py、函数名 test_*,两个都满足才会被收集。 2. 断言失败时 pytest 比 unittest 多给了什么? —— 把表达式算出来的中间值也打出来,比如 where 5 = add(2, 3)。 3. -x 是干什么的? —— 遇到第一个失败就停下,调试时用。测试会写了,下一篇讲 fixture:那些「每条测试都要用的东西」怎么复用。
- ☐ (本篇完,下一篇 11.2 fixture 与 conftest.py)