跳到主要内容
查看文档索引

Vibe Coding 到底要不要写测试?

测试既是 Vibe Coding 的可执行记忆,也可能浪费上下文、拖慢回归并锁死迭代;关键不是写不写,而是为哪种风险购买哪种证据。

Vibe Coding 里有一个很常见的分歧。

一边认为 AI 写代码太快,如果没有测试,每一轮改动都像在拆定时炸弹;另一边认为测试同样是 AI 生成的,它会占用上下文、拖慢回归,还会让一个本来几分钟的改动变成长时间的“修测试”。

这两边都没有说错。

我现在的结论是:Vibe Coding 应该写测试,但不应该把“每次改动都加测试、每次对话都跑全量回归”当成质量。

测试不是免费的正确。它是一种持续产生编写、阅读、执行和维护成本的工程资产。正确的问题不是“要不要测试”,而是:这次改动有什么风险,我们愿意为它购买哪种证据?

为什么 Vibe Coding 反而更需要测试

传统开发里,写代码往往是主要成本。Vibe Coding 改变了这个比例:生成速度变快,理解和验证反而成了瓶颈。

AI 可以在一次任务中改十几个文件,也可以在下一个会话里忘掉上一次为什么这样改。文档能告诉它“应该怎样”,测试则能在它违反约束时立即拒绝。

高价值测试会带来四种收益。

它是跨会话的可执行记忆

一条好的回归用例记住的不是“这个函数曾经这么写”,而是“较慢的旧请求不能覆盖用户后来的选择”。

当实现经历数十轮修改后,原始 Prompt、会话和开发者的记忆都会消失,这条用例仍然能说明系统不能破坏什么。

它会把回归从感觉变成证据

“页面打开了”无法证明鉴权、重试、乱序响应、事务回滚或任务重投正确。这些错误通常不在开发者走的那条成功路径上。

测试可以主动制造失败、并发和边界条件,使“我觉得没问题”变成一组可重复的证据。

它会暴露坏的设计边界

如果一个保存流程必须启动整个页面、连接真实数据库并等待 WebSocket,才能测一个冲突分支,问题不只是“测试难写”。它通常说明业务状态、传输和 View 粘在了一起。

可注入的 Repository、纯状态机和可替换的时钟,不只是为测试方便,它们也在减少运行时耦合。

它能阻止 AI 只修眼前的症状

Bug 修复如果没有回归用例,AI 很容易给当前分支加一个特判。下一次重构时,特判和原因一起被删掉,Bug 就回来了。

先把被破坏的不变量写入 Spec,再用一条会失败的回归用例复现它,修复的就不再只是这一个输入。

测试为什么也会伤害 Vibe Coding

说“测试越多越好”很简单,但它忽略了生成式开发里最稀缺的两种资源:上下文和快速反馈。

第一个成本:浪费上下文

测试代码也是代码。如果每个实现文件旁边都有一个更长的测试文件,再把整套回归日志塞给 Agent,真正的需求、领域不变量和调用链反而会被挤出上下文。

最浪费的通常是这几类测试:

  • 逐行复制实现结构,重构一次就碎一次;
  • 巨大快照,失败时只生成几百行 diff,却说不清哪个行为变了;
  • 同一规则在 Unit、Component 和 E2E 里重复验证;
  • 测试名只有 worksshould render,必须读完全部 fixture 才知道意图。

这些用例不仅占 Token,还会给 AI 提供低信噪比的参考。

第二个成本:回归越来越慢

当项目只有几十个用例时,“改完跑全量”很合理。当它变成多应用、多语言、多存储的工程,全量回归会从反馈机制变成排队机制。

更隐蔽的问题是注意力中断。Agent 停下来等全量测试,失败又发生在不相关包里,一次局部调整就可能变成对历史问题的追逐。回归时间越长,开发者越倾向少跑、晚跑,最后反而降低了测试的实际价值。

第三个成本:过早锁死迭代

探索期的界面、交互和产品语言本来就会快速变化。如果每一个 DOM 层级、CSS class 和中间文案都被测试锁定,AI 不是在维护产品合同,而是在维护某一版实现的考古现场。

这会制造一个很差的激励:为了让测试继续通过,Agent 倾向对旧结构打补丁,不再愿意做本来更清晰的重构。

第四个成本:制造假安全

测试和实现都由同一个 AI 在同一个错误理解下生成,它们完全可能一起错。更糟的是,当现有用例失败时,Agent 可能直接修改断言,让新实现变绿。

所以测试不能自己定义正确。Spec 和用户可观察结果定义正确,测试只负责提供证据。行为真的要改时,先修改合同;没有合同变化时,不能把改断言当成修复。

qrp-platform 给我的现实参考

qrp-platform 很适合观察这个矛盾。它同时包含 Web、Realtime、Worker、Go Backend 和多个领域包;有鉴权、行情、回测、Agent、模拟盘和实盘等不同风险的路径。

在我写这篇文章时,项目最新的一次完整 Web 回归记录已经有 152 个 suite、981 个用例。根回归还会动态收集各 workspace 的测试命令,并发运行多个包。这说明两件事:测试已经成为平台持续变更的基础,全量回归也已经不适合塞进每一个局部编辑循环。

这个项目里有几个做法很值得保留:

  • 局部迭代先跑精确到文件的回归,里程碑再跑完整受影响包;
  • 根回归并发执行 workspace,而不是无条件串行;
  • 测试包装器在成功时保持安静,失败时优先输出失败用例和错误,不把整份 runner 日志倒回 Agent 上下文;
  • Unit 测试使用注入的 Fake 或 Mock,不依赖共享 PostgreSQL 和 Redis;真实基础设施验证与快速单元回归分开;
  • 需求、实现和验证命令写进同一个 Changelog 记录,新会话不需要先读全部测试才知道这次要证明什么。

它也给了一个很好的反例提醒:当回归开始受并发负载、QuickJS 墙钟超时或无关套件失败影响时,解决方案不是要求每轮都重跑所有东西,也不是用无限重试隐藏波动。更合理的方式是让快速回归保持确定,把资源敏感和真实依赖验证放到明确的较慢层。

先按风险决定要不要自动化

我会先把改动分成三档。

风险典型变更默认证据
权限与所有权、资金或交易、持久化、迁移、并发、重试、幂等、协议与数据映射自动回归必须有,同时覆盖失败路径和边界
Store 状态转换、Repository 适配、稳定的用户交互、可复用领域计算一组聚焦的 Unit/Contract 测试,里程碑时跑受影响包
探索期布局、临时文案、纯视觉调整、可丢失原型类型/构建检查和定向人工验收,行为稳定后再固化

还有一个非常有用的判断问题:如果这里在三个月后被 AI 意外改坏,我们能否快速发现,损失是什么?

页面间距错了,通常打开页面就能看到;所有权过滤丢了,用户却可能在数据暴露后才知道。后者显然值得购买更强的自动化证据。

Bug 修复则有一个简单规则:只要 Bug 可以稳定复现,并且将来有回归价值,就先写一条能失败的用例。但不必为一次性脚本、已删除原型或无法稳定观测的暂时环境现象永久增加测试负债。

把反馈拆成四层,不要每次都跑全量

不要让一个命令同时承担“编辑反馈”和“发布准入”。

L0  静态反馈:格式、Schema、类型、编译
L1  聚焦回归:当前不变量和直接相邻路径
L2  组件回归:受影响的 app / package 完整测试
L3  交付回归:全仓、构建、必要的真实集成与关键用户流

一轮 Vibe Coding 的内循环只跑 L0 和 L1;局部实现稳定后跑 L2;只在交付里程碑、发布候选或变更确实跨越多个边界时跑 L3。

这样做不是降低要求,而是把证据放在它最有价值的时刻。如果每改一个变量都跑 L3,开发者最终会想办法绕过它;如果 L1 能在几秒到可接受的短时间内准确拒绝当前错误,测试才会真正进入迭代。

测试应该保护行为,不应该保护实现

一条好的测试应该允许重构。

前端可以把证据拆成三类:

  • Store/Domain 测试负责状态转换、乱序响应、乐观更新、失败回滚和销毁清理;
  • View 测试只负责用户能否发现并执行操作,以及语义、焦点、键盘和状态反馈;
  • 少量浏览器流程负责证明主要边界真的连在一起,不要用 E2E 穷举领域状态机。

后端则可以把领域服务、Repository 合同和真实集成分开。Unit 层注入时钟、随机数、队列、存储和外部客户端;Repository 层验证序列化、命名空间、TTL、缺失记录、依赖失败和错误映射;真实基础设施只在独立的集成层使用可丢弃数据。

不要断言私有函数被调用了几次,除非调用次数本身就是幂等、成本或外部副作用合同的一部分。不要因为组件从 div 改成了更合适的元素就让领域用例失败。

上下文不是靠删测试省下来的

要节省上下文,最有效的办法不是删掉测试,而是给 Agent 一张验证地图,让它可以精确找到这次需要的证据。

一次改动应该能映射成:

需求 / 不变量
  -> owning Spec
  -> 受影响领域或包
  -> 聚焦测试文件与命令
  -> 完整受影响回归
  -> 必要的人工/集成验收

测试就近放在领域边界,文件名和用例名直接表达不变量。Changelog 记录实际执行的命令、范围和结果。这样新会话只需读 owning Spec、相关实现和几个聚焦用例,不需要把整个测试目录加载进来。

运行器的输出也应该服务于诊断:成功时给出简短范围和数量,失败时给出用例名、最小 diff、原始 cause 和相关文件。整页成功日志对 Agent 几乎没有价值。

慢测试要被治理,不能被习惯

测试变慢不是项目成熟的必然代价。对每一层反馈设定时间预算,并持续记录最慢的用例:

  • 把真实网络、真实数据库、容器启动和大模型请求移出 Unit 层;
  • 用可控时钟和确定随机源替代 sleep 和碰运气的时序等待;
  • 按 app/package 边界并发无共享状态的套件,有共享资源的用例明确串行;
  • 缓存可重现的构建产物,但不缓存未经证明的成功结果;
  • 对 Flaky 用例记录 owner、原因和修复期限。可以暂时移出快速门禁,但不能用无限重试把它伪装成稳定。

如果一条用例非常慢,还应该问它到底在买什么证据。一条真实浏览器验收覆盖一个关键链路很合理;一百条 E2E 重复测同一个纯计算函数,就只是在浪费时间。

我会怎样给 Coding Agent 下测试任务

我不再只说“记得加测试”,而会给出下面这个合同:

1. 先读拥有该行为的 Spec,列出本次不能破坏的不变量。
2. 评估失败后果、可发现性和变更频率,决定需要哪一层证据。
3. Bug 修复先用回归用例复现被破坏的合同,再修实现。
4. 断言可观察行为和失败语义,不锁定无关的内部结构。
5. 迭代中只跑静态检查和聚焦回归;稳定后跑受影响包。
6. 全仓、真实依赖和完整浏览器回归只放在匹配风险的交付门禁。
7. 记录实际命令、范围、结果和未执行项,不用“应该通过”代替证据。

这段话的重点不是要求 AI 多写代码,而是防止它把“生成了测试”误当成“控制了风险”。

最后:测试的目标不是覆盖代码,而是保护变化

Vibe Coding 最宝贵的能力是快速尝试。测试不应该杀死这种速度,但速度也不能成为把未验证代码交给用户的理由。

不要追求 100% 覆盖率,也不要只为了省 Token 把测试全部删掉。更值得优化的指标是:每一分钟回归时间、每一段 Agent 上下文,究竟保护了多大的产品风险。

高价值测试会让下一轮修改更快,因为它快速告诉你哪里不能动。低价值测试则会让每一轮修改都更慢,因为它只能证明过去曾经怎么实现。

所以,Vibe Coding 当然应该写测试。只是每添加一条用例前,都值得先问一句:它记住的是产品不能丢掉的行为,还是某一次生成代码的偶然形状?

完 / 继续实践