Guide · 通用

测试全绿,系统却是坏的:我踩过的 7 种「假通过」

9 月 24 日,我在手机上给课题罗盘的学业导师模块(课题罗盘里可选开启的一个模块)建第一个批次,点保存,页面提示「有几项没填对,看下标红的地方」——可页面上没有一格是红的。

查下来,不是手机的问题:这个功能从上线起,就没有任何人、在任何设备上建成过一个批次。

原因很小:建批次的 schema 收了一个 note 字段,表单里却没有备注框。这个字段允许空串,但不允许整个键缺席,于是每一次提交都校验失败。单测一直是绿的,因为测试数据里多塞了一个 note: "",正好把 bug 盖住;前两轮验收都用种子数据里现成的批次,从没在界面上真正建过一个。

修法倒简单:schema 去掉 note,新单测只喂表单真会提交的键,并确认这几条在修复前会变红。

这件事让我把这几个月的坑重新翻了一遍,发现它们是同一类:AI 写的测试、AI 汇报的「已完成」,默认只证明它自己的假设成立。下面是 7 种我亲手踩过的「假通过」。

一、测试绕开了真实入口

智能备课文档生成系统给试点老师开账号之前,我拿一个新账号把完整流程从头走了一遍,第一次传课表,页面只显示 Internal Server Error。

课表在等老师确认教学班时,会停在 awaiting_confirmation 这个状态。产出它的服务层认它,前端类型认它,服务层测试还专门断言了它——只有后端的响应 schema 漏了它。整套测试全绿,是因为服务层测试直接调函数,不经过 FastAPI 的 response_model 校验,这类错只有走 HTTP 才会触发。要是跳过这一步直接开账号,每位老师第一次传课表都会撞上。

修法:补一条走 TestClient 的接口测试,再把 schema 改回漏掉的样子跑一遍,确认它能复现线上那个报错。以后改响应形状,前端类型、后端 schema、HTTP 测试三处一起动。

二、验证脚本替被测代码兜了底

学业导师模块上线前评审,挖出粘贴导入名单的一个 bug:只对库里已有的名字去重,同一次粘贴里的重名照插两条,而文档里写着「整表重复粘贴是安全的」。回头一查,早已上线的班主任模块有同样的缺陷。

验证脚本为什么一直没报?因为它自己带了一个去重集合,正好把这个 bug 掩盖掉了——重复在验证这一层被处理掉,被测代码里的同一个缺陷就没机会暴露。

修法:验证脚本不再自己实现一遍业务逻辑,改成直接调用被测的那个函数;两个模块的导入逻辑抽成一份共用代码,带单测。

三、拿算法的输出当标准答案

我的小说校勘工具阅珍做真实站点验证,第一轮抽了 250 个站点,有效样本 41 个,命中率 100%。看着漂亮,拆开是三条反证:

  • 端到端可用率只有 16.4%:83.6% 的样本在抓取或找页面阶段就被淘汰了,100% 只是幸存者的命中率;
  • 70.7% 的样本和标注一字不差:这些页面的正文本身就是最大的文本块,全是容易题;
  • 禁用选择器提示的对照组照样全中,两组差值为 0;其中 95.1% 的样本单靠纯 DOM 密度这一条就能命中——提示有没有用,这批样本根本测不出来。

同一个项目里还有更隐蔽的:按分类器的判定给测试文件命名,再拿文件名当期望值,差点把误判固化进测试;还拿其中一批当「真正文页」做对照,里面装的其实是被误判的首页。

修法:标注必须来自外部事实,比如「从站点根地址抓的页面,就不该被判成正文页」,而不是被测算法的输出;报告生成器内置告警,有效样本占比过低、满分比例过高时,第一节直接警示,不指望读报告的人自己察觉。

四、指标全正常,成品没法读

阅珍会从多个来源里挑一个当主来源。有个站把正文段落在 HTML 里打乱、靠 CSS 还原显示来防爬虫。抓下来的文本,字数、字符、广告、重复每项指标都正常,它还以「异常 3 条对 8 条」的优势当上了主来源——成品每一章都没法读,而看封面、目录、标题的人工验收也发现不了。

露馅的是一个反常组合:两个来源同一章,长度比 0.99,正文相似度只有 0.25。同样的字,不同的顺序,人工读一章就能确认。

反过来也会出事。混书检测把正文里所有书名号都算作「疑似其他作品」,这个信号自评置信度只有 0.45,却接在一个零容忍的门槛上。《三国演义》三个真实来源分别有 26%、28%、100% 的章节含书名号(引用的《春秋》《临江仙》,还有每章开头本书自己的书名),三个来源全部出局,一本书都组装不出来。改成只认紧跟「请看」这类推荐语的书名后,在同一份 361 章真实正文上,误报从 26%–100% 降到 0;把旧实现换回去,三条新测试全部失败。

修法:验收必须抽样读真实成品;低置信度的线索只能扣分,不能接到一票否决的门槛上。

五、测的是开发者的操作,不是用户的

备课系统上线后,有老师反映换了新课表,「还是显示原来的课表」。后端一路都对,问题在前端:上传框传完没清空,而教务处每次重新导出的课表,文件名都一样。浏览器认为选择没变,不触发 change 事件,等于没传。修法就一行,选完立刻清空 input 的 value。

学业导师模块还有一例。表单写着「第一个是主责」,落库顺序却按复选框顺序,而复选框按姓名排。实测:什么都没改,点一次保存,首页倒计时里的负责人就换了一个人。模块上线时,带登录态的界面一次都没亲眼看过;后来在演示库上按规格把建项目、排节点、看月历和首页倒计时的主路径走了一遍才发现。好在线上还没有真实的学生项目数据。

修法:按真实用户的主路径走一遍,建、编辑、保存、再看下游显示;测试用的文件和数据,尽量是用户真实会产生的样子。

六、测试环境不是真环境

启蒙盒子要在 iPad 上离线玩。项目决策里写着「关卡页全量预渲染,保证离线可达」,Service Worker 也早早配好了。

但开发时用的预览面板根本不支持注册 Service Worker,局域网 http 也注册不了。第三次迭代换成本机 Docker 加无头 Edge、用 DevTools 协议实测,才发现断网后只有首页能打开:自动生成的预缓存清单只有 JS、CSS 这些静态资源,没有页面 HTML。

修法:把全部路由手动加进预缓存,再在能真正注册 Service Worker 的浏览器里断网验一遍,现在首页、选关、任意关卡都能打开。iPad 真机要等 HTTPS 部署后复验,这一步还没做。

七、干活的 AI 自己说「完成」

我在 Codex 里用双线程:主线程用 GPT-6,不写代码,只对照目标做验收;子线程用更快、更便宜的 GPT-5.6 Luna 负责干活。思路很简单,贵的出脑子,便宜的出体力。有一次 Luna 汇报「任务完成」,主线程重新对照目标,发现它只处理了正常流程,异常分支根本没验证。打回去以后,才补上测试和逻辑。

同一个线程写完再自查,很容易顺着刚才的假设往下走,变成自我证明。如果只有一个线程,我大概率也会被那句「已经完成」带过去。开头那个建批次的 bug 修完后,也是另派了一个只读的子代理,把全站表单的字段逐个对了一遍,确认没有第三处。主代理和子代理怎么分工,可以看这篇。

假通过自查清单

检查项防的是
把旧的错误实现放回去,新测试必须变红永远通过的空测试
测试走 HTTP 或界面,不直接调内部函数绕开真实入口
验证脚本调用被测函数,不自己再实现一遍验证层兜底
期望值来自外部事实,不来自被测算法循环论证
抽样读真实成品,不只看指标指标全正常
只喂用户真实会提交的字段和文件数据太干净
在真实浏览器、真机上验环境不对
写代码的和验收的分开自我证明

想让 AI 自己先过一遍这张清单,可以把这段话丢给它:

先别说完成。请做三件事:1. 把修复前的旧实现临时放回去,跑一遍你新加的测试,确认它会失败,并把失败输出贴给我;2. 从真实入口(HTTP 接口或页面表单)走一遍,不要直接调内部函数;3. 测试数据只用真实用户会提交的字段和文件,不要额外补字段。三件都做完,再给我结论和证据。

小结

这 7 种情况里,要么测试全绿,要么指标正常,要么 AI 说完成了,系统照样是坏的。

一个从没失败过的测试,什么也证明不了。先让它失败一次,再相信它的通过。

← 返回指南列表