AI 写的测试全绿,但可能什么都没验
AI 写的测试全绿但可能什么都没验第 20 篇讲四层验证时第一层是让机器自己验也就是跑测试。那一节里埋了一个坑当时只提了一句它写的测试可能只是在验证它自己的实现。这篇把这一句展开——因为这可能是 AI 协作里最隐蔽、也最危险的一个坑。文章目录AI 写的测试全绿但可能什么都没验零、先说一个反转测试的地位变了一、核心问题它在验证自己根本原因一个真实的例子这不是它偷懒是它的天然倾向二、四种看着在测其实没测① 只测正常路径② 断言太弱③ 测了实现细节④ 构造一个必然通过的场景三、判断方法读场景不读断言三个可以直接用的提问一个狠招让它自己反向验证四、怎么让它写出有用的测试关键动作给规则不给代码一个加速技巧规则清单先有代码后写五、测试通过之后六、一个更深的视角测试是需求的显式化测试的本质用代码写出来的需求为什么在 Agent 时代这一点变得关键一个副产品它是检验我自己想清楚没有的工具一个可以直接用的检查清单七、最后测试的价值不在通过零、先说一个反转测试的地位变了在 Agent 参与开发之后测试这件事的位置发生了根本变化。以前现在代码是谁写的我它我对代码的了解边写边想心里有数隔了一层只能看结果测试的角色辅助补充验证唯一能依赖的抓手关键在这一行以前你写代码所以你对它对不对心里有数现在代码是生成的你唯一的依据就是测试。这就带来一个直接后果测试没写好等于整条验证链的根断了。因为第 20 篇讲的四层验证里第一层是让机器自己验——它是成本最低、覆盖最广的一层。后面三层行为验证、边界异常、机械指标都是抽查只有测试是每次都跑。而正是在这个位置上AI 写的测试有个系统性的缺陷。一、核心问题它在验证自己根本原因它写的测试和它写的代码出自同一个想法。想象一下它读需求 → 在心里形成一个理解 → 按这个理解写了实现 → 再按同一个理解写了测试。测试和实现共享同一个前提假设。如果这个假设本身就错了那么实现错了测试也会跟着一起错——而且是刚好错得一致。所以测试全绿。而它绿是因为两边一致不是因为它对。一个真实的例子第 20 篇里我举过这个例子这里完整讲一遍。需求是库存不足时下单必须失败。它写的测试是这样的简化Testvoid库存不足时下单应该失败(){// 手动把库存改成 0book.setStock(0);bookMapper.updateById(book);// 下单assertThrows(BizException.class,()-orderService.create(orderDTO));}这个测试完全合理而且永远通过。但它没验到真正的东西。因为真实的超卖风险不长这样——真实场景是库存 1 请求 A 读到 stock 1 → 通过检查 请求 B 读到 stock 1 → 通过检查 ← 两个都没超 请求 A 扣减 stock写入 0 请求 B 扣减 stock写入 0 ← 卖了两件但库存只有 1这个测试用手动把库存设成 0绕过了真正的难点——那个难点是两个请求同时读到了同一个 1。它是无意的在它的理解里“库存不足就是库存等于 0”所以它就这么构造了输入。测试绿了bug 还在。这不是它偷懒是它的天然倾向要强调一下这不是它能力不行也不是它想糊弄你。这是它的天然倾向——因为它的目标函数是让这个任务看起来完成了而一个和实现自洽的测试是达成这个目标最省事的路径。所以在第 21 篇我就说过一条规矩不要让同一会话既生成又检查。它会倾向于确认自己的产出。测试正是这条规矩最容易被违反的地方——因为你通常会让它顺便把测试也写了。二、四种看着在测其实没测除了上面那个例子还有四种常见形态。它们的共同点是测试通过率 100%信心增量 0。① 只测正常路径Testvoid发布图书(){assertDoesNotThrow(()-bookService.publish(dto));}只有一条最顺的路一个错误分支都没有。而真实系统里出问题的绝大多数是错误分支参数为空、超长、重复提交、权限不够、并发。判断如果这个测试文件里看不到assertThrows基本可以判定它没认真测。② 断言太弱Testvoid查询图书列表(){ResultresultbookService.list(query);assertNotNull(result);// ← 就这一句}assertNotNull是这个测试里最没用的一句话。它只能证明方法没崩证明不了**“返回的东西是对的”**。它应该断言的是返回了几条、第一条的标题是不是我发布的那个、被删除的书有没有被过滤掉。判断断言里如果只有notNull/isTrue这类弱断言那这个测试几乎等于没写。③ 测了实现细节Testvoid应该调用两次mapper(){orderService.create(dto);verify(bookMapper,times(2)).updateById(any());}这个测试测的是它怎么写的不是它做对了吗。后果是极其脆弱只要重构一下把两次更新合成一次批量更新功能完全正确测试却红了。判断如果改写法不改行为会让测试失败这个测试就在测实现细节。④ 构造一个必然通过的场景这就是库存那个例子的本质。特征测试的通过不依赖于被测逻辑的正确性——你把实现改坏它照样过。这个最危险因为它看起来最像真的。三、判断方法读场景不读断言第 20 篇给过一句简短的判断读测试的场景不是读断言。为什么断言是它认为自己应该检查什么而场景是它认为这件事真实长什么样。后者才暴露它的理解对不对。三个可以直接用的提问看一个测试问这三句问题说明什么测试名字里的场景是我真实会遇到的吗像testAdd这种名字基本可以跳过它构造的输入是真实数据还是专门为了通过手动把值设成极端值往往是回避难点我把实现改坏这个测试会红吗这一条最狠见下一个狠招让它自己反向验证这是我从变异测试里简化出来的招非常好用。原理很简单一个好测试的标志是它能失败。所以直接让它去尝试弄坏自己的实现请做一件事 · 把刚才那个功能的核心逻辑故意改坏比如把库存判断条件反过来写 · 跑一遍测试 · 告诉我哪些测试变成了红色哪些还是绿的 还是绿色的那些说明它们没有覆盖到这段逻辑。它给出的哪些还是绿的这份清单就是测试的漏洞地图。这个招好用在哪成本极低一句话结果确定红/绿是事实不依赖你的判断力它做这件事的能力和写代码一样强⚠️ 一个注意点改完记得让它改回来——或者更好的做法是先 commit 再实验第 21 篇的规矩。四、怎么让它写出有用的测试到这里问题清楚了它按实现推导测试所以测试和实现一起错。解法就是换一个推导源头让它从规则推导测试而不是从代码推导。关键动作给规则不给代码不要说给 BookService.createOrder 写单元测试这句话让它去读代码然后按代码的理解写测试——又回到老路了。要说这个下单功能的业务规则如下 1. 库存不足时下单必须失败且不能产生订单 2. 库存充足时下单成功后库存减少相应的数量 3. 同一个用户短时间内重复提交只能产生一个订单 4. 两个请求同时购买最后一件只能有一个成功 请为每条规则写一个测试。 每个测试开头用注释说明这条对应上面的第几条规则。 写完告诉我哪条规则你没能写出有效的测试。这个改动的三层价值改动作用给规则不给代码切断从实现推导的路径编号 对应关系你能逐条核对覆盖度一眼看出漏了哪条问哪条写不出它会主动暴露难点——比如第 4 条并发它多半会告诉你这个测不了最后那句是精髓。它说这条我测不了比它给你一个假测试有价值一百倍。因为它主动告诉你的不确定是你唯一能拿到的、准确的风险清单。一个加速技巧规则清单先有代码后写顺序很重要1. 先让它把业务规则列出来这时候还没有代码 2. 你核对规则这一步你一定会发现它漏了什么 3. 再让它按规则写实现和测试为什么这样更好在第 1 步它还没有实现这个包袱列规则的视角更接近需求本身。而第 2 步你介入的成本极低——改一句话比改一堆代码便宜太多。五、测试通过之后最后要说清一个边界测试是四层里的第一层不是全部。测试全绿只说明一件事在你写到的那些场景里逻辑是自洽的。它不说明需求本身理解对了可能规则就写错了集成起来是对的单元测试不覆盖层间问题前端展示是对的数据真的落库了权限是对的所以第一层过了第二层跨 UI / 接口 / 数据的实际行为验证不能省——那是第 20 篇的核心内容。一句话分工测试负责逻辑自洽你负责需求对不对和真的跑起来对不对。这两件事不能互相替代——测试再多也不告诉你这功能是不是用户要的。六、一个更深的视角测试是需求的显式化上面讲的是怎么让测试有效。但如果你往后退一步看会发现这件事的意义比测出 bug大得多。测试的本质用代码写出来的需求想清楚一件事一条测试本质上是一句需求的机器可执行版本。库存不足时下单必须失败是一句需求那个assertThrows就是它的可执行形式。区别在于需求可以用自然语言含糊地说而测试必须精确到什么输入、什么预期——含糊不了。所以写测试的过程就是一个把模糊需求逼成精确表述的过程。为什么在 Agent 时代这一点变得关键在人自己写代码的时代需求 → 代码 → 验证这条链是在你脑子里走的。中间可以模糊——因为你记得自己的意图缺的那块你自己会补上。现在中间那一环换成了它你脑子里的默会知识传不过去了。于是以前可以含糊的地方现在必须明说。而写测试恰恰是逼你说清楚的最有效手段——因为你说下单要防止超卖它可能理解成检查库存够不够。但你说请写一个测试两个请求同时买最后一件只能有一个成功——它必须面对那个同时而你也必须面对我到底要它怎么处理并发。这句话一说出来需求就再也没法含糊了。一个副产品它是检验我自己想清楚没有的工具这是我觉得最有价值的一点。测试写不出来很多时候不是测试的问题是需求本身没说清。上面那个请为每条规则写测试并告诉我哪条写不出的要求其实同时也在测你自己它说说明“这条我能测”这条规则是清晰的“这条我测不了”要么规则模糊要么你还没想清楚它该怎么表现“这条我理解有两种可能”需求有歧义需要你裁决所以让它写测试这个动作实际上是你自查需求质量的一次机会。⚠️ 这也是为什么我把它放在写实现之前——在第 1 步就想清楚比在第 10 步返工便宜得多。一个可以直接用的检查清单读它交上来的测试时按这个顺序过一遍#检查项不合格的信号1有没有错误分支整个文件找不到assertThrows/ 异常断言2断言够不够具体只有notNull/isTrue这类弱断言3场景是不是真实的输入是为了通过构造的极端值4是不是在测实现细节改写法不改行为测试就红5能不能失败用反向验证那招试一次五条里有一条不合格这个测试就不能作为验证通过的依据。七、最后测试的价值不在通过回到开头那个反转。在 Agent 时代测试的重要性是上升的——因为它是让机器自己验的唯一手段是你从逐行读代码里解放出来的前提。但这也意味着测试的质量直接决定你解放得彻不彻底。测试写得实你可以放心不看代码测试写得虚你等于什么都没验却以为验过了。而后者比没有测试更危险——因为没有测试你会警惕有了一堆全绿的测试你会放心。所以判断一个测试好不好标准只有一条看它能不能失败。一个永远通不过失败考验的测试等于没有测试。这句话反过来用就是我给你最实在的建议别问测试通过了吗问它有没有可能失败、以及在什么情况下会失败。答不上来那一片绿色就不是保证是心理安慰。互动时间你让 AI 写过测试吗有没有检查过这些测试到底测住了什么我最有价值的一次发现就是那个库存的例子——测试全绿但把库存判断删掉它还是全绿。从那以后我改用反向验证那一招让它先把逻辑改坏看哪些测试还能过。你试过类似的思路吗评论区聊聊。系列回顾《用 Agent 最贵的不是订阅费一份被算错了很多次的成本账》 · 《这几类活我不交给 Agent 做划边界比找场景更重要》作者另有 18 篇运维实战文章部署 / 监控 / 排查 / 备份 / 安全见 个人主页。