☰
AI 写的测试全绿,但可能什么都没验
2026/10/15 9:41:57 网站建设 项目流程

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查询图书列表(){Resultresult=bookService.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 篇运维实战文章(部署 / 监控 / 排查 / 备份 / 安全),见 个人主页。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询