Home
avatar

.Sam

【AI Native开发-15】测试失败后应该回到哪里:失败归因与回退地图

摘要

Verify 的价值不只是发现失败,更是判断失败发生在哪一层。一个失败出现后,“把它修好”是本能,但最贵的习惯就是就地把它修绿——因为很多失败的根因在上游:需求没写清、方案错了、测试理解错了,代码只是把上游的错误忠实地呈现出来。

核心论点:

  1. 代码是契约的下游。 很多”代码 bug”其实是 Spec 缺口、Plan 误解或测试错误。在下游补一个上游没做的决定,是把决策欠债藏进代码,它会反复爆。
  2. “修到绿色”是最贵的习惯。 它在当下最快,却用改测试、吞错误、加判断的方式盖住了真正的根因,让同类失败不断回来。
  3. 每次失败都要回流到它真正的源头,并把结论沉淀下来。 失败不是流程的终点,而是让 Spec、测试集、知识库变得更严密的输入。

本篇给出失败分层归因地图、归因决策树、“修到绿色”的代价分析、以及失败如何回写到 Spec、回归集和 pitfalls 知识库。

一个典型场景

还是删除功能。测试报红:已发布文章被普通用户删除时,接口没拦住。

AI 的修法很快:在删除接口里加了一个判断——if (user.role !== 'admin') return 403,测试绿了。

看起来修好了。但真正的根因是什么?回溯发现:

  • Spec 里”服务端二次校验权限”这条规则写得很含糊(REQ-005 只写了”服务端必须校验权限”,没说校验什么、拒绝时返回什么、副作用如何保证不发生);
  • 于是 Plan 里也没把权限校验作为独立一步,只是笼统写”加权限判断”;
  • 实现时 AI 按最熟悉的方式写了个浅判断,没复用项目统一的权限中间件,也没验证副作用;
  • 文本用例里根本没有”普通用户直打接口”这条(TEST-002-4 是评审时才补的)。

这个失败的根因在 Spec(规则不可测、不完备),次要根因在测试设计(漏用例)。在接口里加一行 if 把它修绿,只是把一个上游的决策欠债,用下游的一块胶布贴上——下次另一个接口还会犯同样的错,因为含糊的 Spec 还在、缺失的用例还没补。

正确做法是:修代码止血,但同时回到 Spec 把权限规则写具体、补全权限用例、把”服务端权限必须用统一中间件、拒绝时验证无副作用”写进规范。这样修一次,整个系统的同类错误都少了。

核心问题:为什么就地修绿是最贵的

“修到绿色”的诱惑在于它当下最快。但它的真实成本是延后和放大的:

就地修绿的代价链:

失败 → 改代码/改测试压绿 → 根因(Spec/Plan/测试)没动
  → 同类缺陷在另一个功能里重现(因为含糊的 Spec 还在)
  → 再就地修一次 → 再重现……
  → 代码里堆满"Spec 没提过的判断",没人知道为什么在那
  → 测试被改宽松,失去拦截能力
  → 最终一个压绿的失败在生产爆炸(越权/数据丢失)
  → 排查成本 = 100+

对照错误发现成本梯度(Spec 阶段 1 → Plan 3 → 实现 8 → 测试 20 → 生产 100+):就地修绿本质上是拒绝在成本 1~3 的阶段根治问题,任由它滑向成本 100+ 的阶段。

而回流修复的收益是复利的:把根因修在 Spec,所有基于这个 Spec 做的功能都受益;把结论写进 pitfalls,所有 AI 和人下次都会避开。

失败分层归因地图

一个失败出现后,它的根因可能在六个不同的层。用这张表定位”该回到哪里”:

失败表现最可能根因层该回到哪里关键判据
规则矛盾、目标不清、验收含糊SpecSpec 重写规则两个合理的人会对”正确行为”给出不同答案
技术方案不可行、理解错架构PlanPlan 重做方案Spec 清楚,但 Plan 的技术路径走不通
代码没按 Plan 做、漏了异常路径Implement修实现Plan 对、代码没落实,或代码写错
文本用例遗漏场景、预期写错测试设计补/改用例代码行为符合 Spec,但用例预期和 Spec 矛盾
自动化脚本选择器错、mock 错、断言错自动化用例修测试代码文本用例对,但代码化时走样(如 mock 掉权限)
环境/数据/CI 不稳定测试执行/环境修环境基建本地绿 CI 红、偶发、报错指向依赖/数据
生产出现从未设想的新场景Spec + 回归集补 Spec + 回归用例Spec 压根没覆盖这类情况

用法:不要默认”失败 = 代码错”。 先问”这个失败暴露的是哪一层的缺陷”。判据列给了快速区分的方法。

归因决策树

按顺序走这六步(第 02 篇归因决策树的完整版):

失败出现,逐步判断:

第 1 步:这是产品失败还是测试失败?
  - 系统行为违背 Spec → 产品失败,进第 2 步
  - 系统符合 Spec,但测试报错 → 测试失败,跳到第 5 步

第 2 步:Spec 对这个行为的规定,清楚且可测吗?
  - 不清楚/含糊/矛盾/没规定 → 根因在【Spec】,回 Spec
    (这是最容易被跳过、也最值钱的一步)
  - 清楚 → 进第 3 步

第 3 步:Plan 正确理解了 Spec 和代码现状吗?
  - Plan 漏了这步 / 方案错 / 理解错架构 → 根因在【Plan】,回 Plan
  - Plan 正确 → 进第 4 步

第 4 步:代码按 Plan 实现了吗?
  - 没按 Plan / 漏异常路径 / 写错 → 根因在【Implement】,修代码
  - 按 Plan 做了但还是错 → Plan 本身有盲区,回第 3 步或第 2 步

第 5 步(测试失败):文本用例对吗?
  - 用例预期和 Spec 矛盾 / 漏了场景 → 根因在【测试设计】,改用例
  - 文本用例对,但自动化走样 → 根因在【自动化用例】,修测试代码
    (选择器过期、mock 掉了被测逻辑、断言错对象)

第 6 步:是环境或数据问题吗?
  - 本地绿 CI 红、偶发、依赖缺失 → 根因在【环境/数据】,修基建
  - 偶发失败可能是并发/幂等产品 bug → 别当环境问题压掉,回第 4 步

第 2 步是整套归因里最容易被跳过、又最关键的一步。 人(和 AI)拿到失败的本能是”代码哪儿错了”,很少回头问”Spec 到底说清了没有”。但大量反复出现的失败,根因都在 Spec 的含糊。一个强制动作:任何产品失败,先把对应的 Spec 条目找出来读一遍,确认它是否真的把这个行为规定清楚了。

那条经验法则

贯穿全系列的一条判据,在归因里特别好用:

如果修复动作是”在代码里加一个 Spec 没提到的判断/分支/特例”,那真正该改的不是代码,是 Spec。

例子:

  • 在删除接口加 if (published) reject——Spec 里状态规则写清了吗?没写清,回 Spec;
  • 加一个”管理员例外”分支——Spec 里角色权限定义了吗?没定义,回 Spec;
  • 加一个重试逻辑处理重复提交——Spec 里幂等要求写了吗?没写,回 Spec。

这些判断不是不能加,而是它们代表一个业务决定,这个决定应该在 Spec 阶段被显式做出、经过评审,而不是在调试时由人或 AI 临场拍脑袋。临场拍的决定没有经过”这会不会影响别的场景”的审视,往往顾此失彼。

失败回写:让一次失败变成永久资产

归因完成、止血修复之后,还有最关键的一步——回写。失败只有沉淀下来,才能防止复发。三个去处:

1. 回写 Spec(根因是需求/规则时)

把含糊的规则写具体、把漏掉的场景补上。前面权限例子:REQ-005 从”服务端必须校验权限”改成可测的版本——

REQ-005(修订后):
- 删除接口必须通过统一权限中间件鉴权,禁止在业务函数里手写浅判断
- 非管理员/未登录调用,返回 403/401
- 拒绝时不得执行任何文件操作或副作用(用副作用断言验证)
- 即使前端隐藏了入口,直打接口同样拦截

2. 回写回归测试集(根因是漏测时)

针对这次失败补一条回归用例,它在修复前应该红、修复后绿:

回归用例 REG-xxx:普通用户直打删除接口
- 断言 403 + 文件仍在 + 无副作用
- 永久保留,防止权限校验被重构掉

线上 bug 尤其如此:每个线上缺陷都应转化为至少一条回归用例,织入回归集(第 13/16 篇)。

3. 回写知识库 pitfalls.md(根因是认知/约定盲区时)

把”为什么这么做、容易在哪踩坑”写成永久笔记(第 03 篇上下文工程):

## 权限校验必须走统一中间件

删除等敏感操作的鉴权,必须复用 src/lib/auth 的统一中间件,
禁止在业务函数里手写 if (role === 'admin')。

历史事故:某接口手写浅判断,漏了"拒绝时无副作用"的验证,
导致普通用户删除时文件已被删但返回 403。

配套:拒绝路径必须有副作用断言(文件仍在/数据未变)。

这份笔记会被后续所有 AI 会话读到(它是上下文的一部分),等于把一次事故的教训变成了 AI 下次不会再犯的约束。

线上失败的特殊处理

流到生产的失败,归因和回写要更严格,因为它说明整条验证链都漏了

线上缺陷出现后,必答五个问题:
1. 直接根因在哪一层?(用归因决策树)
2. 为什么 Spec 没拦住?(规则缺口?)
3. 为什么测试没拦住?(漏用例?测试陪跑?层级不对?)
4. 为什么 Verify 没拦住?(覆盖缺口?没在生产形态验证?)
5. 怎么改才能让同类缺陷下次在更早的阶段被抓住?

动作:
- 止血修复(Implement 层)
- 补/改 Spec(根因层)
- 补回归用例,且这条用例要能在 CI 自动跑
- 更新 pitfalls / 评审检查表 / CI 规则
- 复盘"为什么漏到了线上",改进流程而非只怪个人

线上失败是流程改进最宝贵的输入。它以 100+ 的成本发生,唯一的回本方式就是让它推动 Spec、测试集、检查表里的某一项变得更严密。

归因反模式

反模式一:默认代码错。 拿到失败就改代码,从不回头看 Spec。导致上游缺陷反复在不同功能里重现。对策:强制走决策树第 2 步,先读对应 Spec 条目。

反模式二:改测试压绿。 产品失败被当成测试失败,放宽断言让它过。这是最贵的一种——它主动关掉了能抓 bug 的警报。对策:任何断言改动必须引用 Spec 依据,人确认。

反模式三:修完不回写。 止血了就翻篇,Spec 还含糊、用例还缺、pitfalls 没更新。同一个坑几周后再踩。对策:止血 + 回写是一个完整动作,不回写不算修完。

反模式四:只怪个人。 “谁让 AI 写错的 / 谁评审没看出来”。个人会换、AI 会更新,靠追责不能防复发。对策:把教训固化进流程(检查表、CI、pitfalls),让机制防复发。

反模式五:把并发 flaky 当环境问题。 偶发失败一律重试压掉,其中可能藏着幂等/并发产品 bug。对策:偶发失败先排查产品并发缺陷,排除后再归环境。

失败回退总表(贴在工作区)

Spec 层缺陷   → 改 Spec,重过 Gate 1,下游 Plan/代码/测试跟着更新
Plan 层缺陷   → 改 Plan,重过 Gate 2
实现层缺陷    → 改代码,补回归用例
测试设计缺陷  → 改文本用例,重新代码化
自动化缺陷    → 修测试代码(mock/选择器/断言),不碰产品代码
环境/数据缺陷 → 修基建,不为迁就坏环境改产品代码
线上新场景    → Spec 补规则 + 回归用例 + pitfalls + 检查表/CI

人审重点

  • 失败分层的裁决在人:尤其”产品失败 vs 测试失败""Spec 含糊 vs 代码写错”,AI 倾向于归因为”测试太严/代码小问题”以便快速变绿;
  • 任何改测试的动作必须人凭 Spec 确认
  • 回写是否到位由人核对:止血不等于修完,Spec/回归集/pitfalls 三处该回写的有没有回写;
  • 线上缺陷的复盘由人主导,重点是改流程,不是追责。

结语

SDD 不承诺没有失败,它承诺的是:每个失败都有一个明确的归属层,并且不会白白发生——它要么让 Spec 更严密,要么让测试集更密,要么让知识库更深。修到绿色是把失败清零,归因回写是把失败变现。


到这里,个人视角的 SDD 全流程——上下文、Spec、Spike、Plan、评审、Implement、代码评审、文本用例、自动化、调试、执行、Verify、归因——就完整了。但一个人用得好,不等于一个团队用得起来。最后一篇讲规模化:怎么把这套个人方法,变成团队共享的 AI Native 研发系统——统一模板、CI 强制追踪、测试集治理、多 Agent 并行的隔离,以及线上反馈如何驱动整个系统持续进化。

AI Native SDD 失败归因 回退 根因 软件工程