主题
SDD + TDD 双驱动实战:让每条验收标准都落到一个测试
一句话:SDD 定义“做什么”,TDD 保证“做对”,验收标准(Acceptance Criteria,AC)是两者唯一的接口——每条 AC 至少对应一个测试,每个测试都能回溯到一条 AC。换来的是需求、任务、代码、测试四方始终对齐;代价是每次变更都要按 Spec → 测试 → 代码的顺序改三处,不能只改代码。
为什么需要
只写 Spec 不写测试,验收就退化成“人眼对照文档点一遍”。文档写得再清楚,代码有没有真的满足、后续改动有没有破坏,都没有可重复的判据。
只写测试不写 Spec,测试就失去裁判。AI 可以生成大量测试,但它们可能测的是实现细节,而不是用户要的行为;测试全绿,需求却漏了。
两个都做、但各做各的,问题更隐蔽:Spec 里六条 AC,测试文件里几十个用例,没人说得清哪条 AC 被哪个测试覆盖。需求一变,先改代码的人会发现“改没改全”无法验证,Spec 也逐渐变成过期文档。所以双驱动的核心不是把两套流程都跑一遍,而是建立一条可追溯的链:需求 → AC → 任务 → 测试 → 代码。链断了,任何一环都无法自证。
核心机制
AC 必须写成“能直接翻译成断言”的形态。“导入要快”不是 AC,“1000 行在指定环境 30 秒内完成”才是;“错误要友好”不是 AC,“每条失败包含行号、字段、原因,可下载 CSV”才是。
可测试性带来两个约束:一是 AC 要带稳定编号(AC1…AC6),编号在 Spec、任务、测试名、提交信息里复用;二是每条 AC 至少有一个自动化测试,性能、安全这类无法用单元测试表达的,用基准测试或专项检查兜底,不能留空。
端到端流程与人工门禁
图里有两个回退箭头,它们才是流程的价值:AC 写不成断言就回炉 Spec,任务覆盖不全就回炉拆解。如果没有这两个门禁,后面的 TDD 只是在高效地做错事;矩阵则是贯穿始终的账本,任何一条 AC 没有任务或没有测试,都会在这里暴露。
AC × 任务 × 测试映射
| AC | 可测试断言(摘要) | 覆盖任务 | 关键测试 |
|---|---|---|---|
| AC1 格式 | 合法 xlsx/csv 导入成功,入库行数一致 | T2、T4 | test_ac1_imports_xlsx_and_csv |
| AC2 去重 | 已存在 SKU 不更新,skipped 计数准确 | T3、T4 | test_ac2_duplicate_sku_is_skipped |
| AC3 错误报告 | 失败行含行号、字段、原因,可下载 | T6 | test_ac3_error_report_has_row_and_field |
| AC4 性能 | 1000 行在基准环境 30 秒内完成 | T7 | test_ac4_1000_rows_under_30s(基准) |
| AC5 异步 | 提交立即返回 task_id,状态可查询 | T1、T5 | test_ac5_submit_returns_task_id |
| AC6 异常 | 空文件、缺列、编码错误不写入任何行 | T2、T3 | test_ac6_invalid_file_writes_nothing |
任务拆到“一个人半天能做完、能独立验证”的粒度,每个任务标注覆盖的 AC。映射表同时是三个检查工具:拆任务时查“有没有 AC 没人认领”,写测试时查“这个测试在验哪条 AC”,CR 时查“代码改了但测试没动”的异常。测试名带 AC 编号不是形式主义,它让 CI 能自动统计覆盖率,也让后来的人不用读完全部代码就能定位判据。
需求变更的顺序不能反
产品提出“超过 1000 条不要拒绝,截断前 1000 条即可”。正确顺序是先改 Spec 建立新判据,再改测试让它变红,然后改代码变绿,最后跑全量回归。
变红这一步不能省。它证明新测试确实能区分旧行为和新行为;如果改完测试直接就是绿的,要么断言写错了,要么旧代码已经碰巧满足,两种情况都需要重新检查。先改代码则相反:测试被改成迎合实现,Spec 还停在旧需求,下一个人按 Spec 开发又会冲突。
人/AI 分工速查
| 阶段 | 人必须做 | AI 自动做 | 人可抽查或跳过 |
|---|---|---|---|
| Spec 起草 | 对齐需求、定边界、回答澄清问题 | 起草 Spec、生成 AC、标注不可测项 | 不可跳过 |
| 设计 Plan | 评审方案与接口,寻找更简单的替代 | 生成模块、接口、错误码与任务计划 | 不可跳过 |
| 评审拆解 | 确认 AC 无遗漏、任务粒度不超过半天 | Spec 自检、AC 映射任务、画依赖图 | 不可跳过 |
| TDD 红 | 抽查断言是否覆盖 AC 的子条件 | 按 AC 写测试、跑测确认失败 | 逐个看测试可跳过 |
| TDD 绿/重构 | 只审重构是否过度设计 | 写实现、自动跑测、失败自修、重构 | 绿灯循环可全程不看 |
| 集成验证 | 对照 Spec 逐条验收、跑性能基准 | 全量测试、基准报告、AC 覆盖矩阵 | 不可跳过 |
| Code Review | 架构、业务、安全、镀金检查 | 坏味道扫描、变更摘要、覆盖率检查 | 不可跳过 |
实战场景
两个场景对应追溯链的两种断法:链被 hotfix 拦腰打断,以及链的更新顺序被倒置。共同点是断链当时都“全绿”,代价要延迟到别人接手时才显形。
事故复盘:追溯链断在一行手改
故障现象与代价。一次生产 hotfix 手工改了一行:if rows > MAX_IMPORT_ROWS: reject 改成截断前 1000 条。服务恢复了,故障单和群里都写了,Spec 和测试没回写。两周后的常规迭代里,AI 按 specs/import/spec.md 的旧条款继续实现 AC1,一次提交把截断逻辑覆盖回“超限直接拒绝”;同一个故障在线上二次发生,这次撞上的是存量客户的批量数据,客诉加赔付处理持续三天(事故口径,按业务校准)。
排查回退过程。第一次被覆盖时,有人看着 diff 嘀咕过“这行代码上个月不是这么写的”,然后没有下文;第二次复发后 git blame 回溯,四个环节完全脱节:
- Spec:AC1 仍是“超过 1000 条直接拒绝”,没动过。
- 测试:按 AC1 编写、全绿——它保护的是被 hotfix 废弃的旧行为。
- 代码:hotfix 改过又被 AI 改回——唯一携带真实信息的环节,也是唯一可被覆盖的环节。
- 提交信息:只有
fix: hot patch,没带 AC 编号——对任何基于编号的自动检查不可见。
四方各自都“对”,合起来让修复消失。AI 按 Spec 和测试写代码,而这两处都没有这次 hotfix——这不是 AI 特有问题,换成新人接这张工单同样会覆盖回去。
修法:追溯链四件套 + CI 卡点。本篇生产动作的“三件套同批提交”(Spec diff、测试 diff、代码 diff)补上第四件:提交信息带 AC 编号并与 Spec 一致,例如 fix: AC1 truncate over-limit imports (spec+test+code)。CI 增加两个卡点:
- 提交信息无 AC 编号、或编号在当前 Spec 中不存在时阻断;PR 改了导入相关行为测试却没有 Spec diff 时,标记并要求人工确认。
- hotfix 例外通道:紧急允许先恢复服务,24 小时内补齐四件套并挂工单(经验值,按团队 SLA 校准);超时未补齐转为团队级阻断项。
为什么机制对症。本篇“为什么需要”说“链断了,任何一环都无法自证”,这起事故是它的可视化版本:覆盖回退靠人审拦不住——评审者看的是代码 diff,不会追问“这行是不是某次 hotfix 的产物”。四件套让每次行为变更在四处留下同一证人,缺一处 CI 就红,把断链的代价从“三个月后没人能发现”改成“当场提交不过去”。
案例:需求变更顺序反了的下场
故障现象与代价。正文给了“超过 1000 条不拒绝、截断”的正确顺序:先改 Spec,再改测试让它变红,然后改代码变绿。一个团队反着做——赶发布,先改代码,口头承诺“测试和文档回头补”。回头补没有发生。半年后另一需求要用 AC3 错误报告的 reason 字段做失败自动归类,才发现实现早已改成面向用户的文案,补写的测试也照着文案全绿,而 Spec 里仍是机器可读的编码语义。发现时归因功能已进入验收,只能连返工一轮,历史报表数据全部重刷(事例规模,按团队校准)。
还原损失。顺序反了之后,测试是从实现反推出来的,生下来就是绿的——它永远不可能对旧行为失败,也就永远不可能证明新旧行为有区别。表面状态是回归全绿、覆盖率不降、代码在跑,实质是 AC3 的语义被静默改写且没有任何载体见证:全绿不再证明“需求满足”,只证明“实现自洽”。这正是本篇“AI 生成的测试可能同义反复”那条边界的人工版本,也解释了最小上手为什么要求“只给 Spec,不给实现代码”。事故当时的两处“真相”长这样:
text
Spec(仍是旧的) AC3:每条失败含行号、字段、原因,可下载 CSV(reason 为枚举错误码,供下游归因)
实现与测试(已成新的)reason 返回用户可读文案;断言照此编写,全绿变红时刻不可补办。它是全流程里唯一能证明“测试能区分新旧行为”的机会,先改代码等于把它作废:断言写出来时实现已是新行为,没有人能再回答“这条新测试在旧代码上红过吗”。
纠偏边界。顺序反了不是不可救,但补救代价不对称:变更当天回写 Spec 是十分钟,半年后考古真实语义是几天。所以纪律要有机器兜底——Spec diff 与测试 diff 的同批校验见 03-SDD规格驱动 实战场景的 CI 卡点设计,“回头补”的承诺不兑现就阻断,而不是靠人记得。
给团队的一句话。顺序不是流程仪式,它决定裁判是谁:先改 Spec,实现接受裁判;先改代码,裁判给实现盖章。AI 会放大这个选择——它把哪一环当真相,就以哪一环为准生成后续一切。
最小上手
前提:仓库里已有测试框架和 CI。把下面两段存成 spec.md 与 tasks.md,先人工过一遍再交给 AI 执行。
markdown
# Spec: 商品批量导入
## 目标
运营通过 Excel/CSV 批量导入商品,异步执行,可查看结果与错误明细。
## 不做
- 不做增量更新:重复 SKU 只跳过,不覆盖
- 不做图片、多语言字段导入
- 不做跨租户导入
## 验收标准(AC)
- AC1 格式:上传 .xlsx 或 .csv,表头含 sku,name,price;合法文件导入成功。
- AC2 去重:SKU 已存在时跳过且不更新原记录;结果包含 skipped 数量。
- AC3 错误报告:每条失败含行号、字段、原因,可下载 CSV。
- AC4 性能:1000 行、单机 2C4G、MySQL 8.0,导入在 30s 内完成(经验阈值,按环境校准)。
- AC5 异步:POST /api/import 在 500ms 内返回 task_id(经验阈值);GET /api/import/{task_id} 查状态。
- AC6 异常:空文件、缺列、编码错误返回明确错误码;任务失败且不写入任何行。markdown
# Tasks: 商品批量导入
- T1 任务表与状态机:import_task(id,status,total,success,skipped,failed)→ AC5
- T2 解析器:xlsx/csv 转统一行结构;空文件、缺列报错 → AC1,AC6
- T3 校验与去重:字段校验、批量查已有 SKU → AC2,AC6
- T4 落库:按批写入,明确事务边界与失败回滚 → AC1,AC2
- T5 异步 API:提交任务、查询进度 → AC5
- T6 错误报告:收集行级错误并生成 CSV → AC3
- T7 性能:批量查询 + 批量写入,跑 1000 行基准 → AC4每个任务进入 TDD 时只做一件事:从它标注的 AC 翻译测试,先确认红灯,再写实现。测试名统一用 test_ac<编号>_<行为>,提交信息带 AC 编号,例如 feat: T2 parser (AC1, AC6)。AI 写测试时只给它 Spec 和当前任务,不要给实现代码,否则它会照着实现写断言。
生产动作
- 门禁放在三个位置:Spec 评审通过才能拆任务;任务覆盖检查通过才能进 TDD;集成验收通过才能进 CR。门禁不是审批仪式,每一道都要有可检查的产物(AC 列表、映射表、验收报告)。
- AC 覆盖率单独统计:代码覆盖率回答“哪些行被执行”,AC 覆盖率回答“哪些需求有判据”。CI 收集测试名后与 Spec 的 AC 列表比对,缺一条就阻断合并。可用
pytest --collect-only -q | grep -oE 'test_ac[0-9]+' | sort -u做最小检查,再与 Spec 比对。这只是 Python/pytest 的示例,其他语言用等价方式:统一测试命名规范(如 AC 编号前缀或标签),在 CI 脚本里解析测试清单再与 Spec 比对,不要写成工具无关却只给 pytest 命令。 - 变更必须三件套一起提交:Spec diff、测试 diff、代码 diff 在同一个变更里。只有代码 diff 的提交默认打回,因为它无法证明需求同步了。变更描述里写清“改了哪条 AC、哪个测试由红转绿”。
- 抽查红灯与映射,而不是只看绿灯:随机抽 1–2 个测试,确认它在旧实现下真的失败、断言检查的是行为不是 mock 调用次数;CR 时再随机挑一条 AC 追问“哪个测试证明它”,答不上来就说明链路断了。永远为绿的测试比没有测试更危险。
- 性能类 AC 要固定环境:记录机型、数据量、数据库版本,否则 30 秒这个阈值无法复现,验收会变成口头承诺。
边界
- 探索性原型不适用:需求还没成形时,写 Spec 和测试会锁死方向;先用可运行原型找问题,稳定后再补规格。
- AC 覆盖不了全部质量:可维护性、安全边界、交互体验很难写成断言,仍需 CR、威胁建模和人工试用,不能因为测试全绿就放行;同时 AC 不是越多越好,堆二十条模糊 AC 不如六条可执行 AC。
- AI 生成的测试可能同义反复:照着实现写断言、mock 掉关键依赖、只测 happy path,都会制造虚假安全感;红灯确认和抽查是必要成本。
- 流程开销真实存在:小改动也走完整套会拖慢交付;判断标准是“变更是否跨需求边界”,一行文案修正不需要重新开 Spec。
- Spec 会漂移:如果变更只改代码不改 Spec,三个月后文档就不可信;把 Spec 放进仓库、和代码一起 review,是唯一低成本的约束。
讲清楚
SDD 和 TDD 是一条链上的两个环节:Spec 把模糊需求压成可断言的 AC,TDD 用测试把 AC 变成可执行判据,代码只是让测试变绿的载体。串联的关键不是工具,而是 AC 编号在 Spec、任务、测试名、提交信息里始终一致,任何需求都能查到测试,任何测试都能追回需求。
需求变更时顺序不能反:先改 Spec 建立新判据,再改测试让它变红,然后改代码变绿,最后跑全量回归。先改代码则测试失去裁判,Spec 也会过期。代价是前期多花时间写清 AC、拆任务,收益是返工和扯皮变少;探索性原型不值得上这套流程。
适用:2026 年主流 SDD/TDD 工具链 | 初稿 2026-10