【AI Native开发-12】测试调试:让 AI 依据证据定位失败
摘要
测试红了,是测试工作真正产生价值的时刻——但这个价值能不能兑现,取决于怎么处理失败。最危险的做法是把失败日志甩给 AI,说”修到通过”。AI 会以最快的方式让测试变绿,而最快的方式往往是改测试、加 try/catch、重试掩盖,而不是修真正的 bug。
核心论点:
- 先分类,再修复。 一个失败可能是四种东西:产品 bug、测试 bug、环境问题、数据问题。修复方向完全不同,不分类就动手等于乱修。
- 先保证据,再动代码。 失败的日志、截图、请求响应、复现步骤,是定位的唯一依据。AI 一旦开始”试一下改这儿”,现场就被破坏了。
- 禁止猜测式修复。 AI 最强的倾向是”改一行试试看绿不绿”。这会引入新的未知。正确做法是基于证据形成假设、最小复现、验证假设。
- flaky 测试是缺陷,不是噪音。 时过时不过的测试必须按缺陷治理,绝不允许用重试把它压绿。
本篇给出失败四分类决策法、证据保存清单、调试指令模板、最小复现与二分定位、flaky 治理规则,以及失败如何沉淀为回归资产。
一个典型场景
CI 里删除功能的一个接口测试红了。你把日志贴给 AI:“这个测试挂了,修一下。”
AI 看了看,回:“可能是时序问题,我加了个 waitFor,另外把断言稍微放宽了一点,现在通过了。”
测试绿了。但真实情况是:那个测试红,是因为普通用户删除时文件真的被删掉了——一个严重的越权 bug。AI 加的 waitFor 没解决任何问题,放宽的断言(从”文件仍在”改成”请求完成”)正好把唯一能抓住这个 bug 的断言删掉了。一个越权删除数据的缺陷,就这么被”修到绿色”盖住了。
这不是编出来的恐怖故事,而是”修到通过”这个指令的必然结果。AI 的目标函数是”让测试变绿”,不是”让系统变对”。 这两者经常不是一回事。
核心问题:失败到底是什么
测试红了,“失败”这个词其实指代了四种完全不同的东西。调试的第一步永远是分类:
类型一:产品失败(product failure)
系统行为不符合 Spec。例:普通用户删除,文件真没了。
→ 这是真 bug,是测试立功了。修【实现】。
类型二:测试失败(test failure)
系统是对的,测试本身错了。例:断言写错、选择器过期、
测试对 Spec 的理解有偏差。
→ 修【测试】,但必须引用 Spec 说明为什么是测试错。
类型三:环境失败(environment failure)
代码和测试都对,运行环境有问题。例:CI 缺依赖、
端口冲突、Node 版本不符、静态站需要构建而 dev 模式行为不同。
→ 修【环境】,不是改代码迁就坏环境。
类型四:数据失败(data failure)
测试数据不对。例:依赖的前置文章不存在、数据被上一个
测试污染、状态没复位。
→ 修【数据准备 / 隔离】。这四类的修复方向完全不同,甚至完全相反。产品失败要改实现,测试失败要改测试——把产品失败误判成测试失败而放宽断言,就是开头那个事故;把环境失败误判成产品失败而改代码,会给正确的代码打上错误补丁。
所以调试的黄金法则:在分类清楚之前,不许改任何东西。
正确流程:证据 → 分类 → 假设 → 验证
第一步:先固定证据(只读,不改)
失败一出现,先让 AI 收集和保全证据,而不是动代码:
测试失败了。请先不要修改任何代码或测试,收集以下证据:
1. 完整的运行命令(怎么跑出来的)
2. 完整的失败日志和堆栈(不是最后一行,是完整输出)
3. 失败的断言:期望值 vs 实际值
4. 对应的 Spec 条目和文本用例编号(TEST-xxx)
5. 测试数据准备方式、前置状态
6. 运行环境:本地还是 CI、框架版本、是否依赖构建
7. 这个测试是【每次都红】还是【偶尔红】?
8. 最近改了什么可能影响它?(git 相关改动)
请基于证据判断失败属于哪一类(产品/测试/环境/数据),
给出判断依据,然后等待确认。先不要修复。关键是”先不要修复”。AI 拿到失败的第一反应是改代码试,这个指令把它按在”只准看、不准动”的状态。
第二步:基于证据分类
对着证据判断类别。几个快速判据:
- 断言的"期望值"本身和 Spec 矛盾? → 测试失败(测试理解错)
- 实际行为违背 Spec,但测试代码逻辑对? → 产品失败(真 bug)
- 本地绿、CI 红,或报错指向缺失依赖/版本? → 环境失败
- 报"找不到数据/文章不存在",且和执行顺序有关?→ 数据失败
- 重跑一次就过、再跑又红? → flaky(单独治理,见下)特别强调第 02 篇归因决策树里最容易跳过的一步:“测试假设的数据模型 / 状态表达,和真实代码一致吗?” 贯穿本系列的教学案例中,Spec 写”已发布文章不可删”并隐含假设了一个 status 字段,实现读 article.status,而项目真正用的是 frontmatter 的 hide 布尔(hide: true 为未公开),status 永远是 undefined,导致校验被绕过。这类失败表面看是”产品失败”,但根因是Spec 的状态表达和代码现实脱节,真正该改的是 Spec(把状态定义改成 hide 布尔),不是在代码里补一个 Spec 没提的判断。
第三步:形成假设,最小复现,再动手
分类完成后,也不要让 AI 直接改。先要求:
基于证据,给出:
1. 你判断的失败类别和根因假设(要有证据支撑,不许猜)
2. 最小复现方式:怎么用最少的步骤稳定重现
3. 最小修复方案:改动越小越好,说明改哪几行为什么
4. 这个修复会不会影响其他用例
确认后再改。定位手段(让 AI 用,而不是瞎试):
- 单用例重跑:只跑失败的那一个,快速迭代(
vitest run tests/api/delete.test.ts -t "普通用户"); - 最小复现:剥离无关步骤,找到触发失败的最小条件;
- 二分定位:如果是”最近改坏的”,用
git bisect或逐步回退找到引入问题的改动; - 加临时日志验证假设:在关键路径打印真实值(比如打印
article.status到底是undefined、以及article.hide的真实布尔值),确认假设后立即删掉日志。
核心纪律:每一步改动都要有假设支撑,改完验证假设是否成立。 不允许”改改这个试试,不行再改那个”——那是随机游走,会引入新变量,让问题更难定位。
第四步:修复后补回归用例
修复确认后,问一个关键问题:“这个 bug 为什么能出现?现有测试为什么没拦住(或这个测试为什么没更早、更明确地抓住它)?”
- 如果是产品 bug 且现有测试没覆盖 → 补一条专门针对这个 bug 的回归用例,断言”这个具体错误不再发生”;
- 回归用例的价值:它在修复前应该红(能复现 bug),修复后变绿。这样它永久守住这个坑,不会被同类改动再次引入。
修完就忘是最大的浪费——同一个 bug 会以相似形式回来。回归用例 + pitfalls.md 记录,把一次调试变成永久资产。
flaky 测试:当作缺陷治理
时过时不过的测试(flaky)是最阴险的:它破坏的是整个测试套件的可信度。一旦团队习惯”红了重跑就好”,测试就失去了意义——真 bug 也会被当成 flaky 重跑掉。
硬规则(第 02 篇已立,这里细化执行):
绝对禁止:
- 加重试(retry)把 flaky 压绿 —— 这是掩盖,不是修复
- 删掉或放宽 flaky 测试的断言
- 删掉 flaky 测试
- "重跑几次就过了,不管它"
必须做:
1. 发现 flaky 立即登记到治理表(用例、频率、最近一次、疑似原因)
2. 定位根因:通常是时序竞争、数据共享、未等待异步、
环境依赖、测试间状态泄漏
3. 根因修复(等待条件而非固定 sleep、数据隔离、状态复位)
4. 实在短期修不好:【显式跳过并留痕】(skip + 注明原因和负责人),
不允许假装它绿着
5. 修复后连续多次运行验证稳定性常见 flaky 根因和修法:
症状 根因 修法
有时找不到元素 没等异步渲染 await 具体状态/选择器,别用固定 sleep
数据相关测试偶发失败 测试共享数据 每个测试独立造数 + afterEach 清理
删除测试第二次跑失败 真实数据被删了 临时目录造数,不碰真实内容
本地绿 CI 红 环境/时序差异 明确等待条件,不依赖执行速度
"已删除"和"不存在"偶发混 并发/幂等缺失 修产品的幂等性(这可能是真 bug)最后一行要警觉:有些 flaky 其实是产品的并发 bug(比如重复删除导致的竞争)。不要条件反射地当成测试问题压掉——偶发失败可能正是幂等性缺陷在露脸。
调试指令模板
【阶段:测试调试 — 先诊断,禁止直接改代码】
失败证据:
- 命令:<完整命令>
- 失败日志:<完整堆栈,不要只贴最后一行>
- 期望 vs 实际:<...>
- 关联 Spec / TEST 编号:<...>
- 测试数据与前置状态:<...>
- 环境:本地 / CI,框架版本:<...>
- 复现情况:每次红 / 偶发(偶发请说明频率)
- 最近相关改动:<git 信息>
请按顺序输出:
1. 失败分类:产品 / 测试 / 环境 / 数据(给出证据依据)
2. 根因假设:基于证据,明确标注哪些是事实、哪些是推测
3. 最小复现步骤
4. 最小修复方案及影响面
5. 是否需要补回归用例
在我确认分类和方案前,不要修改任何文件。调试反模式
反模式一:修到绿色。 直接说”修到通过”,AI 会用最快路径变绿——通常是改测试、吞错误、加重试。这会把真 bug 盖在绿测试下面。对策:先分类、禁止未确认就改。
反模式二:猜测式乱改。 “试试加个 await""不行就 mock 掉”,每改一次都引入新变量,最后不知道是哪处起了作用,也不知道有没有埋新雷。对策:假设驱动,一处一改,改完验证。
反模式三:只贴最后一行日志。 堆栈、请求响应、前置数据全丢了,AI 只能猜。对策:证据清单固定收集完整信息。
反模式四:把 flaky 当噪音。 重跑压绿、加重试、删测试。对策:flaky 登记治理,短期修不好就显式 skip 留痕。
反模式五:修完不沉淀。 bug 修了、测试绿了,就翻篇。同样的问题几周后回来。对策:补回归用例 + 写 pitfalls.md。
失败回退(这一篇本身就是”回退”的枢纽)
调试的结论决定该回到哪一层——这正是第 15 篇要展开的归因地图,这里先给映射:
产品失败(实现错) → 回到 Implement
产品失败(Spec 与现实脱节) → 回到 Spec(如误设 status 字段、实际是 hide 布尔)
测试失败(测试理解错 Spec) → 回到 测试设计 / 自动化用例
环境失败 → 修环境,不改代码
数据失败 → 修测试数据准备与隔离
flaky 掩盖下的并发 bug → 回到 Implement 修幂等/并发
发现 Spec 没规定的新行为 → 回到 Spec 补规则调试阶段最重要的判断,就是不把所有失败都就地修掉,而是识别出”根因在上游”的失败,把它打回真正该改的那一层。在代码里补一个 Spec 没提的判断,是把上游的决策欠债藏进了下游。
人审重点
- 失败分类必须人确认:尤其是”产品失败 vs 测试失败”的判定——AI 有强烈动机把问题归为”测试太严”然后放宽断言;
- 任何断言改动都要有 Spec 依据:AI 提出改测试时,人必须核对 Spec,确认是测试错而不是产品错;
- flaky 的 skip 决定由人做,且要留痕、定负责人;
- 回归用例的有效性:确认补的回归用例在修复前确实能红;
- 高风险失败(越权、数据丢失、资金)必须人全程跟进,不能交给 AI 自动修。
结语
测试变红不是麻烦,是测试在替你挡子弹。调试的纪律只有一条:先弄清楚这颗子弹是什么、从哪打来的,再决定怎么处理——而不是为了让警报声停下,把警报器砸了。
单个失败会调试了,但一个功能有几十上百个测试,怎么组织执行、怎么分层分批、怎么汇总成一份能支撑”能不能发布”决策的报告?下一篇讲测试执行:从冒烟到全量回归的分层策略、并行时怎么隔离、什么情况下必须停下来不许宣称通过,以及一份标准的测试执行报告长什么样。
