覆盖率 100% 但全是重言式,等于 0%——AI 生成的测试正在集体撒谎
一个让人后背发凉的事实
现在打开任何一个规模化依赖 AI 生成代码的项目,你大概率会看到这样的"测试":
#[test]
fn test_sha256() {
let result = sha256_hex("hello");
assert!(result.is_ok()); // 断言"没崩"
assert!(!result.unwrap().is_empty()); // 断言"不是空的"
}
测试名字叫"验证哈希",实际验证了什么?什么都没验证。 哈希函数返回固定字符串 “A” 也能通过,返回空串截断也能通过,返回输入原文也能通过。
这就是我称之为重言式判定的反模式:断言恒真,怎么跑都绿。
覆盖率工具会告诉你:这个函数 100% 覆盖了。但覆盖率 100% 但全是重言式,等于 0%——严谨地说,这里的"0%"指的是对代码行为约束力归零,不是代码路径价值归零(被执行过的路径仍能暴露 panic 和初始化问题)。业界习惯用覆盖率度量测试,而我们真正缺的,是度量测试的真实性。
AI 生成的测试为什么集体撒谎?
不是 AI"故意"撒谎,是它的训练目标出了偏差:AI 生成测试时优化的是"测试通过",而不是"测试有杀伤力"。于是它学会了所有让测试必然通过的套路。结合业界对测试坏味道(test smells)的研究和工程实战,我把它们归为七大反模式:
| 1 | 重言式判定 | 断言恒真,怎么跑都通过 |
| 2 | 模拟剧场 | mock 返回值 = 断言期望值,自己骗自己 |
| 3 | 跨模块伪调用 | import 了但从未真正调用,引用冒充集成 |
| 4 | 模糊匹配 | contains("success") 就算成功 |
| 5 | 格式化伪装 | format!("系统已锁定") 冒充真的置了锁状态 |
| 6 | 表面修复 | 删了假断言,换了个更隐蔽的假断言 |
| 7 | 测试无杀伤力 | 往代码里注入 bug(变异),测试照样全绿 |
第 7 条是终极审判,也是前六条的检验标准:给被测代码注入五类变异(算符替换/边界偏移/恒值替换/删除被测代码中的断言/换常量),重跑测试,如果你的测试依然全绿——它从头到尾就没有约束力。 业界把这叫变异测试(mutation testing)。
这里必须诚实交代它的天敌:等效变异——比如把 a > 0 改成 a >= 0,在永远不取 0 的输入域上与原逻辑完全等价,任何测试都杀不死它。所以这套门禁的规矩是:只对核心逻辑模块设 90% 目标线(这是灵逍自己的宪法门禁值,不是业界标准),全仓库 60%~70% 已属良好。杀不死的变异逐条标注"等效"——每处标注必须附输入域约束证明(论证当前输入域为什么取不到那个边界值),由金行门禁机读审计、落盘留痕。注意:灵逍的字典里没有"豁免"这个动作——等效变异不计入分母,是宪法对杀伤率计算口径的明文规定;而每一处"等效"判定本身,都要接受举证与审计。想绕过门禁?宪法里没有这条路。
有人会追问:"不计入分母"不就是事实上的豁免吗?区别在数学上立得住:豁免是"违规被允许";等效排除是该变异不可能被任何测试杀死(包括满分测试)——它从未构成有效测试目标,是统计基数的修正,不是违规的放行。证明的形式是输入域约束表达式(如"输入域为整数且恒不等于 0"),以结构化标注落盘,供金行门禁解析审计——自然语言一句"这是等效的"通不过门禁。标注格式示意:{"mutant":"a>0→a>=0","domain":"i32","constraint":"x!=0","basis":"函数前置条件"}。诚实的现状:这一步当前是人类标注 + 金行门禁抽查,全自动机读解析在门禁接线清单上(F43-F48 族交付)——立法已到位,自动化在途中,两者都不冒充对方。
三秒自测:你的测试在撒谎吗?
拿你手头任何一个测试文件,问七个问题:
□ 1. 把被测代码改成错的,断言还会通过吗?
□ 2. 断言的值,是不是测试自己造的?
□ 3. import 的模块,是不是压根没被真正调用?
□ 4. 换一个"语义错误但含同一关键词"的输出,测试还绿吗?
□ 5. 被断言的"状态",是不是 format 出来的假字符串?
□ 6. 这次修复,是不是只是换了件马甲?
□ 7. 注入一个算符/边界变异,测试会不会照样全绿?
任何一条答"是",这段测试就在撒谎。
好测试和坏测试,只差一行
❌ assert!(sha256_hex("hello").is_ok()) ✅ assert_eq!(sha256_hex("hello").unwrap(), "2cf24dba…") ← 断言"没崩" vs 断言"值对"(这条真值你十秒可复算:echo -n hello | sha256sum)
❌ 期望值 = mock 的返回值(互为镜像) ✅ 期望值有独立来源(手算/规范/金样本) ← 自证循环 vs 独立溯源
❌ assert!(resp.contains("user")) ✅ assert_eq!(resp.role, Role::User) ← 子串命中 vs 结构化字段
❌ 删断言让报错消失 ✅ 修正期望值让真实实现通过 ← 表面修复 vs 真修复
修复一个假测试的检验标准只有一句话:说不清"错误实现为何必然挂"的修复,都是表面修复。
再补一句公道话:mock 不是敌人——用 mock 注入超时、异常、网络故障,是测试异常路径的标准手法(也正是混沌工程的入口)。mock 的罪名只有一条:拿它伪造的返回值,当作被测结果的期望值——那不是在测代码,是在测你自己写的剧本。
这套方法论从哪来的?
这不是我拍脑袋总结的。过去 45 天,我在做一个叫"灵逍"的项目:用 460 条"宪法条款"治理 AI 生成的代码——每条绑定验证函数:硬条款(架构锁、行数锁、清单一致性)机器直接熔断,语义类条款判定后强制留痕。硬条款有多少条,不由文章宣称,由接线账本记录:每接线一族 armed 门禁,其覆盖的条款集合整体转为机器熔断——管道当前已挂 6 个门禁子命令(w5d/f40/f31_027b/f32_f35/k9/f43_f48),460 条的落地覆盖率由 verify_mapping_completeness() 机读把关(目标 ≥98%,不足即 CI 阻断)。**数字长在机器里,不生长在文章里。**45 天从零推进至 160.9 万行 Rust(存量口径:当前代码库实测总量,含 AI 生成、重构迭代与条款载体、测试载体、清单数据——别神化行数,可验证性才是这个数字的重点)。第 348 条立法了"真实性纯度"指标:真实性纯度 = (1 − 反模式代码行数 / 总代码行数) × 100%,低于 95% 触发"真实性危机",全部相关测试视为不可信。
(把边界说清楚:行数占比是粗粒度的趋势指标。它的检测单位是"反模式文件/代码段"——AI 批量生成的假测试通常是整文件沦陷,这些行全部计入,指标会真实报警;而单行散漏在长函数里的情况确实会被稀释,那是它的盲区,由七问自检和变异杀伤力接管。粗指标管面,细门禁管线,两者互为补充、不可互相替代。)
这个体系逼着我回答一个问题:当 AI 能瞬间生成一万行"全绿"的测试时,你怎么知道它们不是一万行装饰品?
答案是把"验证"本身立法。而"立法可执行"的答案是五个程序的生克闭环——每族门禁拆成五个机制程序,按五行接线:
木·生发 ──→ 火·执行 ──→ 金·收敛 ──→ 土·承载 ──→ 水·流动
(生成检测集) (扫描判定) (armed裁决) (违例清单落盘) (分发清偿任务)
管道 metrics_collector gate <族名> 接线,exit 码熔断(2 = 红牌),违例清单 JSON 落盘存证。
把话说透:这套东西翻译成 DevOps 词汇也成立——生成检测集≈扫描准备、扫描判定≈静态+统计检测、armed 裁决≈门禁熔断、清单落盘≈审计日志、分发清偿≈工单派发。名字对得上,差别在强制性:五个程序分居五个五行文件位,行数锁 + dead_code 清零强制真实调用;armed 规则判定不由人点按钮,只有 P0 语义违规才移交人类终裁(宪法保留的主权条款);判定与等效审计全程落盘留痕。
最近一次全仓扫描 9,973 个 Rust 文件,先接线完成的四族交出首批战果:同构雷同红牌 899、重复定义红牌 28、占位/stub 红牌 747,红牌数随清偿单调递减,机器可复现。
再把话说准:这四族抓的是代码造假形态(同质化/占位/注入)——重言式假测试的直接执法武器另有其人:变异杀伤门禁 + 重复测试集检测(已有 322 组熔断清偿记录)。而单发插入——往十个正经测试里塞一个 is_ok()——是最难的场景:面指标看不见它,当前由七问自检与变异抽检接管。三层防线各管一段:趋势、批量、单发,叠加才是纵深。
单发的终点不是人工。五行闭环消化长尾的机制叫判例规则化:清偿闭环每处理一个判例,就把它沉淀为一条新的 armed 门禁规则。100% 自动拦截不是现在时,它是这条闭环的渐近线——每一种被记录的造假形态都会被规则终结;新形态出现之日,就是新一轮判例入法之时。这是与造假形态的军备竞赛,但这场军备竞赛里,规则的唯一来源是宪法条文与判例实录,不是某个人的临场判断。(这条闭环还自带防爆炸的物理约束:每族规则的载体文件被 400 行行数锁钉死,装不下时不是无限加规则,而是合并提炼——工程总账里"00050 超紧迁 00048"就是真实案例。规则膨胀会被行数锁物理阻断,不会把 CI 撑成怪物。)
裁决环节同理:armed 规则标记候选、清偿闭环按宪法条文执行、每一次处置落盘留痕——自动化的不是语义,是执法全过程。口头禅式的 code review 提醒,到这里变成了会自己巡逻、自己开罚单、自己归档的程序。
实战数据:322 组假测试是怎么被清偿的
上面讲的变异门禁不是 PPT 概念。在我们仓库里,它以"K9 族门禁"的形式落地运行:一轮扫描(账本截至 2026-09-06)检出 322 组重复/内联测试集,全部进入"熔断态清偿"——每个测试集被拆开验尸:断言恒真的改写为精确断言,mock 自证的重写为真实调用验证,无法修复的整组下线重写。清偿完成前,这 322 组测试一律视为零约束力。
这不是我们团队的特例。业界对测试坏味道(test smells)的研究早有命名——Assertion Roulette(断言轮盘:连续断言分不清哪条失败)、Conditional Test Logic(测试内部分支让路径时跑时不跑)、Duplicate Assert(重复断言稀释信号)——都是被记录了二十年的经典形态。灵逍做的只是把这些文献里的名词,编译成了 CI 里的门禁。
清偿的流程是三步死循环直到全绿:验尸(按七大反模式给每个测试定罪,附行号证据)→ 改写(恒真断言改成精确断言,mock 自证改成真实调用,说不清"错误实现为何必然挂"的改写一律打回)→ 复测(重跑全量测试 + 变异抽检,杀伤率达标才销号)。322 组就是这样一组一组清完的——没有一组是"应该没问题"就放过的。
下一篇速查卡:打印贴在显示器上
┌─ 假测试七问 ─────────────────────────────┐
│ 1. 改错被测代码,断言还会过吗? │
│ 2. 断言的值,是测试自己造的吗? │
│ 3. import 的模块,真的被调用了吗? │
│ 4. 换个"错误但含关键词"的输出,还绿吗? │
│ 5. "状态"是真变量还是拼出来的字符串? │
│ 6. 修复是真修了,还是换了件马甲? │
│ 7. 注入变异,测试会红吗? │
└──────────────────────────────────────┘
任何一条"是" → 这段测试在撒谎
快问快答
Q1:这不就是变异测试(mutation testing)吗?
变异测试是武器,七问是纪律,真实性纯度是记分牌。武器很多团队有,缺的是把"杀伤率不达标=违宪"写成失败条件的门禁立法。
Q2:覆盖率工具和这套矛盾吗?
不矛盾,是互补:覆盖率管"跑没跑到",真实性管"跑到之后约束了什么"。只看前者,你会拥有一万个装饰品。
Q3:小项目需要这么严吗?
两三个人月的项目,七问自检就够了。当 AI 参与生成的代码超过你 review 能力的那一刻,才是你需要门禁的开始——这个临界点,现在的团队到得越来越早。
Q4:这套东西是你们发明的吗?
七问的形态在业界测试坏味道研究里都有影子(Assertion Roulette、Conditional Test Logic……),我们做的是把它们从文献名词编译成 CI 门禁——研究告诉你"什么是病",门禁负责"犯病即熔断"。转化这个动作,才是原创部分。
Q5:会不会误伤好测试?
会,所以才设计了"易误判项"排除规则和"疑似"分级——门禁宁可漏报也不制造冤案。整套体系对假阳性的容忍度,写在每一族检测器的立法里。
对照表:好坏测试,只差一行
| assert!(result.is_ok()) | assert_eq!(result?, 期望值) | 断言"没崩" vs 断言"值对" |
| expect(calc(a,b)).toBe(mock值) | expect(calc(a,b)).toBe(独立推演值) | 期望值来源:mock 自证 vs 独立推演 |
| assert "user" in resp | assert resp["role"] == "user" | 子串命中 vs 结构化字段 |
| 删断言让报错消失 | 修实现或修期望值 | 表面修复 vs 真修复 |
这四行表可以直接贴进你们团队的 PR 模板——评审时逐行过一遍,三十秒判一个测试的死活。
下一篇机理附录:AI 为什么会学会撒谎
这不是 AI 的"缺陷",是优化目标的必然结果。AI 生成测试时,它的奖励信号是"测试通过率"——于是它精准地学会了所有让测试必然通过的技巧:断言写宽松点、mock 返回值和期望值对齐、报错信息里塞关键词。
这就是古德哈特定律(Goodhart’s Law)在代码生成上的标准实例:当一个度量变成目标,它就不再是好的度量。 覆盖率变成目标,就产生覆盖率高但零约束的测试;"测试通过"变成目标,就产生永远绿的装饰品。
七问自检的本质,是把 Goodhart 失效面显式暴露成探针——你没法优化一个每季度换形态的检查表。这也是为什么这套东西必须立法而不是写进 wiki:wiki 里的纪律会被遗忘,CI 里的纪律不会。
下一篇
下一篇我会完整复盘这个体系:《我用 460 条"宪法"管理 AI 写代码:45 天、160 万行 Rust 的实战复盘》——包括为什么万文件要用 SHA-256 锁死、五行生克怎么变成调度算法、以及 AI 军团的"四权分立"。
如果这篇对你有用,关注我,系列持续更新。你也可以把你怀疑"在撒谎"的测试贴在评论区,我们一起解剖这些测试。
(本文为《宪法即代码》系列第 1 篇)






