> 用 any() 写的回归测试是假保证——它只要求最容易过的那个夹具通过
断言"至少有一个输入被正确处理"的测试,等于没有断言。 一组夹具里只要有一个好走的,它就永远绿,而 bug 恰好住在其他几个里。
结论先行¶
断言"至少有一个输入被正确处理"的测试,等于没有断言。 一组夹具里只要有一个好走的,它就永远绿,而 bug 恰好住在其他几个里。
回归测试要断言全集:数量对得上,且逐个都对。
# 假保证:三周全绿,而线上正在丢东西
check(any(kind == "topic-phrase" for kind, _ in queries))
# 真断言:立刻红
check(len(phrases) == len(spans), f"{len(spans)} 个引号跨度,只活下来 {len(phrases)}")
for span in spans:
check(f"{span[:32]!r} 原样保留", " ".join(span.split()) in phrases)
为什么¶
真实的一次。一个工具承诺"引号里的内容原样变成查询",
测试就是上面那行 any(...)。夹具用了三个人名,
三个都在长的多词引号跨度里,全部超过 10 字符——
而管线上的过滤器只杀短跨度。于是这条测试测的恰好是唯一不会失败的那个形状,
一路绿了三周,期间线上每次都在丢 8 到 11 字符的论文名。
改成计数相等加逐个断言,当场变红。
同一次事故还有第二半,是同源的:提交信息声称的行为不算行为。
那次修复的提交信息原话是 "Quoted phrases become queries verbatim"——
代码没有做到。同一批提交还加了一条降级覆盖率警告当安全网,
那条警告拿注册表里的键(s2)去比论文记录里的字段(semantic_scholar),
两者永远不相等,所以安全网也是坏的。
一次修复里两处都只写了意图、没写断言,于是同一个失败又发生了两次。
怎么用¶
三条,按顺序:
-
存在量词换成计数相等 + 逐元素。 见到测试里的
any、in、assert len(x) > 0、assertTrue(result),先问一句"全都对吗"。>= 1这种断言的信息量是零点几个 bit。 -
夹具要覆盖输入的形状,不是覆盖个数。 上面那次缺的形状是 "短名字"和"只有一个实词"。写测试前先列出输入的极端形状 (最短 / 最长 / 单 token / 全大写缩写 / 带连字符 / 空),每种至少一个夹具。 三个同形状的夹具不比一个强。
-
提交信息里每一句关于行为的话,配一条断言它的测试。 配不上的那句就改成"打算",别写成陈述句。 半年后没人会去核对那句话,但所有人都会相信它。
唯一的验收标准:这条测试在 bug 存在时会不会红? 验证方法只有一个——把 bug 放回去跑一遍。 新写的测试第一次跑就是绿的,说明它什么都没证明。
这和"手写的自检清单永远全绿"是同一个根,只是下沉了一层: 那边是检查项的集合从记忆里来,这边是单条断言的量词太弱。 见 手写的自检清单永远是全绿的——它只包含你刚修过的东西; 发现了却不执行的那一类见 自审的失效模式不是"没发现",是"发现了没执行"。
出处¶
~/Tools/scholarSpyder/tests/test_topic_override.py,2026-09-07 从any改成逐跨度断言;坏掉的安全网在scholar_scout.py的 degraded-coverage 警告(v3.3 加入,v3.4 修)- 被它放过去的那个 bug:为噪声写的过滤器会精确杀掉最强的那条输入——显式指令必须走旁路