附录定位:写到手生时翻的口袋卡。命令、断言、夹具、替身、节律五张表,配合正文章节的完整演练使用;每张表都标注了对应的章号,忘了来龙去脉就回去重读那一节。
| 场景 | 命令 | 说明 |
|---|---|---|
| 跑全部测试 | pytest |
按目录递归收集 test_ 开头的文件 |
| 跑单个文件 | pytest tests/test_cart.py |
定位到具体文件 |
| 跑单个用例 | pytest tests/test_cart.py::test_empty_cart_total_is_zero |
双冒号接函数名 |
| 关键字过滤 | pytest -k "discount and not coupon" |
表达式匹配用例名 |
| 只看上次失败 | pytest --lf |
复跑上轮失败的用例 |
| 失败即停 | pytest -x |
第一个失败就中止 |
| 显示打印内容 | pytest -s |
等价于 capture=no |
| 静默进度 | pytest -q |
只输出点与摘要 |
| 显示用例名逐条跑 | pytest -v |
冗长模式,CI 日志常用 |
| 生成覆盖率 | pytest --cov=src --cov-report=term-missing |
需要 pytest-cov 插件 |
| 查看最慢用例 | pytest --durations=10 |
排出耗时前十名 |
| 随机顺序 | pytest --randomly-seed=last |
pytest-randomly 插件提供,可复现上次顺序 |
命令行里最值钱的组合是 pytest -x --lf:先跑上次失败的那批,一红就停。改代码的间隙反复用它,反馈周期可以压到秒级——这正是红绿重构循环对运行器的要求。
pytest 的断言就是普通 assert,失败时它会做内省重写,把表达式各部分的实际值打印出来,不需要记忆 xUnit 风格的 assertEqual 家族:
import pytest def test_assertion_zoo(): total = 0.1 + 0.2 assert total == pytest.approx(0.3) # 浮点比较必须用 approx assert "优惠" in cart.summary() # 包含判断直接用 in assert cart.items == [] # 集合相等直接比 assert user.age >= 18 # 比较运算符原生支持 def test_raises(): with pytest.raises(ValueError, match="金额不能为负"): checkout.pay(-5) # match 用正则校验消息 def test_warns(): with pytest.warns(DeprecationWarning): legacy.render() # 断言警告类型
浮点断言是新手事故高发区:assert 0.1 + 0.2 == 0.3 在二进制浮点世界里必挂。金额计算建议全程用整数分或 decimal.Decimal,测试里照样用 approx 兜底。
import pytest @pytest.fixture def cart(): """每个用例拿到全新购物车,互不污染。""" return Cart() @pytest.fixture(scope="module") def price_table(): """加载一次、多处共用的只读数据,模块内只装配回。""" return load_price_table() @pytest.fixture(autouse=True) def clean_tmp(tmp_path): """自动生效:每条用例跑完清现场。""" yield shutil.rmtree(tmp_path, ignore_errors=True) @pytest.mark.parametrize("password,level", [ ("short", 0), ("alllowercase1", 1), ("Str0ngPass", 3), ]) def test_password_level(password, level): assert rate_password(password) == level
记住三条就够:fixture 是带名字的"原料供应",用例把它当参数写就自动注入;yield 之前是装配、之后是拆卸;scope 只在确实昂贵的资源上放大,默认函数级最安全。参数化用来表达"同一行为、多组输入",别拿它凑用例数量。
from unittest.mock import Mock, patch, call def test_checkout_calls_gateway(mocker): gateway = mocker.Mock(return_value={"status": "ok"}) checkout = Checkout(gateway=gateway) checkout.pay(order) gateway.charge.assert_called_once_with(amount=order.total) def test_retry_on_timeout(mocker): gateway = mocker.Mock(side_effect=[TimeoutError, {"status": "ok"}]) assert checkout.pay(order).status == "ok" assert gateway.charge.call_count == 2
替身家族一句话区分:dummy 占位不参与行为,stub 喂固定答案,spy 记录调用轨迹,mock 预设期望再校验,fake 是能跑的简化实现(内存数据库是典型)。边界判断口诀:替身只替"你拥有协议的东西"——自己写的接口、自己定的存储抽象;第三方 SDK 与未实现的模块优先包适配层再替。
| 阶段 | 动作 | 时长上限 | 退出条件 |
|---|---|---|---|
| 红 | 写一条会失败的测试,跑一遍亲眼看它失败 | 分钟级 | 失败信息与预期一致 |
| 绿 | 写最小实现,只求通过当前测试 | 分钟级 | 全部测试转绿 |
| 重构 | 消除重复、改命名、提函数 | 分钟级 | 测试保持绿,代码更好读 |
| 提交 | 小步提交,留下循环轨迹 | 每轮次 | 仓库处于可发布状态 |
三定律贴在显示器边上:在没有失败的测试之前,不写产品代码;只写刚好失败的测试,别超前;只写刚好让测试通过的代码,别顺手发挥。红灯超过半小时就说明测试切得太大,退回去拆。
| 信号 | 多半说明 | 去哪章看对策 |
|---|---|---|
| 断言越写越像实现本身 | 测试耦合了内部结构 | 第 3、4 章 |
| mock 满天飞、改接口要改十处测试 | 替身越界替了不该替的 | 第 3 章 |
| 覆盖率很高、线上照样出事 | 度量错了对象 | 第 7 章 |
| 循环一转就是半天 | 测试切太大或环境太重 | 第 2、5 章 |
| 团队觉得"没时间写测试" | 反馈闭环没建立 | 第 7 章 |
这张附录刻意做薄:速查卡的价值在"翻得快",不在"讲得全"。每个条目背后的取舍与陷阱都在正文的对应章节里展开过——查表是起点,重读才是归宿。