Python 22:哪些测试能够证明业务不变量
把 SQLite 的 cancel 暂时替换为“直接返回 True、实际不改任务状态”。调用方拿到“取消已接受”,重新打开数据库却仍看见 queued。新加的回归测试在 state == "cancelled" 处失败;恢复真实方法后,累计 68 项测试通过。这个故障不是抽象的“测试覆盖率提高”,而是把一条承诺——取消排队任务后查询能看见 cancelled、不能再执行——交给能反驳它的断言。
第21 篇的 SQLite 事务已经留下了真实数据库和失败注入。本篇不另造一套 tasklab,只整理测试的责任边界,并用一次受控变异确定测试确实会对业务规则的破坏报警。
一个不变量要在哪些地方观察
进程内单元测试用 create_store → submit → cancel → query,检查返回 True、状态 cancelled、结果 None、之后执行被拒绝;它证明当前 Python 对象之间的状态流转。数据库集成测试用临时文件运行相同四个动作,另建 SQLiteTaskGateway 再查状态和结果;它额外检查了真实 SQLite 写入与重开连接可见性。两个测试复用业务规则,但不能互相替代:只测字典不会发现忘记提交事务,只测数据库很难把失败定位到应用层还是仓储层。
故障测试使用 experiments/22_mutation.py 中的 patch.object(SQLiteTaskGateway, "cancel", return_value=True),只在这次演示的测试作用域内替换仓储方法;它没有修改生产源码,也没有把存储层 mock 掉:测试仍对真实临时 SQLite 文件提交和重新查询。这种变异刻意切断“返回接受”与“实际状态变化”,预期测试失败恰有一条。unittest 打印 FAILED (failures=1) 是被测测试组的结果;包裹脚本确认确有一条失败后以 0 退出,表示故障被检测到,不是声称变异版本也通过了回归。
不要把一次绿色覆盖成全部保证
仓储测试 test_injected_failure_rolls_back_state_and_result 实际创建数据库,注入写回中断,再重新查询 queued / null;单纯断言 connection.execute 被调用不能证明持久化可见性。CLI 测试又启动新的解释器执行 submit、run、query,分别验退出码和标准流。unittest discover -s tests -v 是累计入口,报告真实的单元、集成和故障路径;通过计数不能证明所有调度时序都覆盖。运行中取消、跨进程强杀、服务断线及副作用与结果提交的先后问题仍需后续章节设计控制事件,不靠在测试里睡几十毫秒猜调度。
在 examples/python-language/ 下运行:
1 | |
后一条的标准错误含一条 AssertionError 与 FAILED (failures=1),标准输出记录 "detected_failures": 1;外层命令退出 0,因为已经核实测试拒绝了被破坏的规则。未注入时全套 68 项通过。原始双通道记录、环境和断言在 examples/python-language/evidence/22/20261007-tests/,不把预期失败行从日志里删掉以制造“全绿”。
预测题
只保留 assert gateway.cancel(task_id) 而删掉后续查询、结果和拒绝重复执行的断言;上述受控变异还会被这条测试抓住吗?
答案:不会。变异方法也返回 True,断言看不出数据库是否改变。需要读取任务状态,最好用新仓储实例重新查询,才能抓到漏写或漏提交。原测试未删掉这些断言;演示保留了真实数据库检查。
变更题
把 sqlite 文件换成内存字典,单元测试通过,能证明另一个进程还能查询旧任务吗?如果不能,保留什么检查?
答案:不能。字典位于当前进程,离开后状态消失。需要用真正的临时 SQLite 文件、新仓储实例乃至子进程查询保存的状态,并在事务故障后重新查询确认两列都回滚。本章及 21 的实测覆盖这两类情形;跨主机、长时间运行和任意文件系统故障没有测。
23 进一步研究替换时钟和注入故障时哪些真实资源必须留在测试中。系列导航只指向完成的页面。




