乐于分享
好东西不私藏

AI 写测试为什么特别爱 mock?一篇论文给了数字

AI 写测试为什么特别爱 mock?一篇论文给了数字
如果你让 AI 帮你补过单元测试,可能见过这种场面:它先熟练地 import mock,再把数据库、网络请求、时间、配置、内部服务全都替换掉,最后跑出一排绿色测试。

看起来很专业。

但你隐约会有一点不安:这到底是在测业务逻辑,还是在测 mock 写得够不够像真的?

这个问题现在有了更具体的数据。Andre Hora 和 Romain Robbes 在论文《Are Coding Agents Generating Over-Mocked Tests?》里分析了 2025 年真实仓库中的 120 多万次提交,覆盖 2168 个 TypeScript、JavaScript 和 Python 仓库。其中包括 48563 次 coding agent 提交、169361 次修改测试的提交,以及 44900 次给测试加入 mock 的提交。

结论不复杂:Coding Agent 确实更爱碰测试,也更爱往测试里加 mock。

这不是说 mock 一定有问题。恰好相反,mock 是工程测试里非常正常的工具:第三方 API、支付网关、邮件服务、慢数据库、随机时间、外部网络,这些东西不该每次单元测试都真的跑一遍。

问题在于,AI 很容易把“合理隔离”写成“把真实世界全部隔离掉”。

1. AI 为什么喜欢 mock?

先别急着怪模型偷懒。站在 Agent 的角度,mock 是一条很顺的路。

第一,mock 能让测试更容易跑通。真实依赖可能需要环境变量、数据库、Docker、网络、账号权限,任何一个环节没配好,测试就会失败。Agent 的目标通常是“让测试通过”,而不是“证明系统在真实路径上可用”。mock 掉依赖之后,问题少了很多。

第二,mock 能让生成更确定。模型擅长补模式:函数调用了 userService.getUser(),那就 mock 一个返回值;代码检查 payment.status,那就构造一个 paid。这种测试和实现代码贴得很近,生成难度低,也容易一次通过。

第三,Agent 往往是在“看过答案”之后写测试。Reddit 上有开发者把这个问题说得很直白:让 AI “给这段代码写测试”,它会先读实现,再写出匹配实现的测试。这样的测试不是从需求出发,而是在描述现有代码做了什么。如果实现里本来就有 bug,测试可能会把 bug 当成正确行为固定下来。

这就是 AI 测试里最微妙的地方:它不是不努力,它努力的方向可能错了。

2. 过度 mock 会制造什么假象?

最常见的假象是“覆盖率很好,风险没降多少”。

一个典型例子是订单创建流程。真实路径可能包括库存检查、优惠计算、支付预授权、订单落库和消息发送。如果测试把库存服务、支付服务、数据库写入和消息队列全部 mock 掉,最后只断言“createOrder 被调用了一次”,这当然会绿。

但这类测试没有回答几个关键问题:

  • 优惠金额和库存扣减是否真的一致?
  • 数据库事务失败时会不会留下半条订单?
  • 支付成功但消息发送失败时如何补偿?
  • 上游接口字段变化时,当前代码会不会静默吞错?

mock 越多,测试越像一间布景齐全的舞台。灯光、台词、走位都对,但门后面没有真实房间。

论文作者也提醒了一个类似风险:带 mock 的测试可能更容易自动生成,但它们验证真实交互的能力可能更弱。因此,团队不能只要求 Agent “补测试”,还要明确告诉它哪些地方可以 mock,哪些地方必须走真实 wiring。

3. mock 不是坏,坏的是没有边界

比较实用的规则是:mock 你不拥有的东西,少 mock 你自己的业务逻辑。

可以 mock 的对象通常包括:

  • 第三方 API

  • 外部支付、短信、邮件服务

  • 慢而不稳定的网络调用

  • 不可控的时间、随机数、系统环境

  • 昂贵或不可重复的外部资源

要谨慎 mock 的对象包括:

  • 领域服务之间的核心协作

  • 内部业务规则

  • 数据转换和校验逻辑

  • 关键持久化路径

  • 权限、金额、状态机等高风险逻辑

Reddit 的 PracticalTesting 讨论里也有类似经验:可以 mock 不归你控制的 API,可以 mock 慢或不稳定的东西,但不要随便 mock 自己的领域逻辑,并且至少保留一些使用真实 wiring 的测试。

这句话值得写进团队的 Agent 配置里。

4. 给 Agent 写测试时,提示词要换一种问法

“帮我给这段代码补测试”通常不是好问题。

更好的问法是先把测试输入从“实现代码”切到“行为约束”。例如:

请先根据下面的需求写测试计划,不要读取实现细节:1. 列出 happy path、边界条件和失败路径2. 标明哪些依赖可以 mock,哪些必须使用真实实现3. 对金额、权限、状态流转类逻辑,优先写行为测试4. 每个测试说明它验证的业务事实,而不是验证某个内部方法被调用5. 生成测试后,再根据测试结果修改实现

如果你已经有实现代码,也可以加一道审查:

请审查这些测试是否过度 mock:1. 哪些 mock 替代了我们自己的业务逻辑?2. 哪些断言只验证了 mock 调用,而没有验证用户可观察行为?3. 哪些测试即使真实集成坏了也会通过?4. 哪些场景应该补一条集成测试或端到端测试?

这个流程的重点不是追求“纯 TDD”。现实项目里,大多数人不会严格按红绿重构的教科书流程工作。但至少要避免一种情况:Agent 读完错误实现,再生成一套为错误实现背书的测试。