AI 协作项目里,如何引入"测试先行"

github:github.com/YueJingGe/c...
不只是口头提倡 TDD,而是把 Red-Green-Refactor 写进 AI 协作入口,让它在正确的时间被自动触发。

一、什么是测试先行

测试先行(Test-Driven Development,TDD)不是先写代码再补测试,而是:

  1. 先写一个会失败的测试(Red)。
  2. 运行它,确认它因为目标行为缺失或 bug 存在而失败。
  3. 写最少代码让测试通过(Green)。
  4. 在测试全绿的前提下重构(Refactor)。

一句话概括:没有先失败的测试,就不写生产代码。

在我的项目里,TDD 被写进了 .agents/skills/test-driven-development/SKILL.md,作为 AI 协作的默认动作之一。它不是为了追求覆盖率,而是为了在 AI 频繁参与编码时,把"改动必须可验证"变成机械习惯。

二、为什么 AI 协作项目更需要 TDD

AI 写代码很快,但也容易:

  • 顺手改多:你以为它只修了一个按钮状态,结果它把附近三四个文件都动了。
  • 证据缺失:它说"修复完成",但你不知道它有没有先复现 bug。
  • 回归隐蔽:实现通过了,但旧行为被悄悄破坏。

TDD 的"先失败、再实现"正好克制这些问题:

  • 测试是契约:AI 在动手前必须先理解"期望行为是什么"。
  • 失败是证据:没有 Red 输出,就不能声称 bug 已修复。
  • 回归被即时捕获:每次改完跑全量测试,break 立刻暴露。

三、为测试先行做了哪些设计

1. 工具链:和 Vite 同生态

前端测试栈选的是 Vitest + @testing-library/react + @testing-library/jest-dom + jsdom。

ts 复制代码
// web/vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: "jsdom",
    setupFiles: ["./src/test-utils/vitest.setup.ts"],
    include: ["src/**/*.test.{ts,tsx}"],
    exclude: ["node_modules", "dist"],
  },
});
ts 复制代码
// web/src/test-utils/vitest.setup.ts
import "@testing-library/jest-dom/vitest";

2. 文件组织:测试与源码同目录

不建 __tests__/ 目录,测试文件和被测源码放一起:

这样找测试不需要跳转目录,AI 在改源码时也能立刻看到同目录的测试约束。

3. 规则落点:Skill + 门禁 + 文档/计划三层互锁

AGENTS.md 是 AI 进入项目时的入口,里面用 按改动类型触发 的方式规定:

  • 改 web/src/** 逻辑:读 .agents/context/frontend-context.md + docs/harness/frontend-rules.md;新增、修改或修复组件、hook、工具函数、状态或交互行为时调 test-driven-development skill,并补/改同目录 *.test.ts(x)

TDD 不是一份 Skill 文件就能讲完的事,而是三层互锁:

层级 含义 职责 载体
Skill 层 即时行为指令:执行 Red-Green-Refactor 怎么做 .agents/skills/test-driven-development/SKILL.md
门禁层 机器兜底:提交前检查逻辑文件是否带同目录测试 有没有做 scripts/check-frontend-tdd.mjs + husky pre-commit/pre-push
文档/计划层 保留上下文与验收标准,引用 Skill 和门禁而不重复规则 为什么这么做 docs/harness/frontend-testing.md、spec/exec-plan 模板

4. 质量门禁:check:all 与 TDD 专检

根目录的 package.json 把测试串进聚合门禁:

json 复制代码
"check:all": "npm run check:harness && npm run check:frontend-tdd && npm run format:check && npm run lint:check && npm run stylelint:check && npm run test --workspace=web && npm run build:web"

check:frontend-tdd 是 TDD 专检:对本次 git 变更中 web/src/** 的逻辑文件,检查同目录是否存在 *.test.ts(x);fix 类提交还必须带有测试文件变更。它被挂在 .husky/pre-commit 和 .husky/pre-push 上,作为机器兜底。

任何改动完成,必须通过 harness 自检、TDD 专检、Prettier、ESLint、Stylelint、前端测试和构建,才算闭环。

四、TDD 什么时候被触发

项目里判定是否走 TDD:不看用户 prompt 里有没有出现"TDD"三个字,而是看代码改动的性质。

必须走 TDD 的情况:

  • 新增功能 / 新组件 / 新工具函数
  • 修复 bug(含恢复预期行为)
  • 修改行为、交互或数据流
  • 重构

不强制单元测试的情况:

  • 纯样式 / 布局 / 响应式改动(改走 frontend-visual-verification 视觉验证)
  • 配置改动
  • 类型声明改动
  • 文档改动

这个口径被写进了 AGENTS.md 和 docs/harness/frontend-testing.md,避免 AI 因为用户措辞不同而漏执行。

五、触发之后都做了什么

一旦判断需要 TDD,AI 会按下面的顺序执行:

  1. 读上下文 :先读 frontend-context.md + frontend-rules.md + test-driven-development/SKILL.md。
  2. 写测试:根据需求或 bug 写一个最小测试。如果是 bug,测试要能复现 bug。
  3. 跑测试拿 Red:运行单文件测试,确认失败原因是"行为缺失"或"bug 存在",而不是拼写错误。保留失败输出。
  4. 写最小实现:只写让测试通过的代码,不提前设计。
  5. 跑测试拿 Green:确认测试通过,且其他测试没坏。
  6. 重构(可选):在测试全绿下清理代码。
  7. 跑 check:all:通过完整质量门禁。

整个过程的核心不是"有测试",而是测试必须先失败,并且失败原因被确认。

六、四个验收场景

下面是项目里真实跑过的 4 个场景,分别对应新增函数、修 bug、改组件行为、重构。

场景 1:新增纯工具函数

任务:新增 truncateText(text, maxLength),文本超过 maxLength 时截断并追加省略号。

场景 2:Bug 修复

任务:修复 copyToClipboard,当 navigator.clipboard.writeText 抛错时,降级使用 document.execCommand('copy')。

原 copy.ts 只走现代 Clipboard API,一旦 writeText 被拒绝就直接抛错,没有降级路径。

场景 3:组件行为变化

任务:改造 MessageList 组件,让 AI 回复中的 markdown 在气泡里正常渲染(换行、列表点、粗体、代码块)。

场景 4:重构

任务:抽取滚动到底部判断逻辑,复用到多个地方。

过程:

  • 写 web/src/utils/scroll.test.ts,覆盖 30px 阈值边界:已在底部、阈值内、31px 外、远离底部、滚过底部。
  • Red:scroll.ts 不存在,失败。
  • 抽取 checkIsAtBottom 到 web/src/utils/scroll.ts。
  • 更新 App.tsx 导入并使用新工具函数。
  • Green:单文件测试通过,再跑 npm run check:all。

七、门禁层:机器兜底

TDD 治理有机器兜底:scripts/check-frontend-tdd.mjs。

它检查两件事:

  1. 本次变更的 web/src/** 逻辑文件是否带同目录 *.test.ts(x)。
  2. fix 类提交是否同时变更了测试文件。

检查范围排除了入口文件、.d.ts、测试文件本身和纯类型/样式改动。pre-commit 检查工作区/暂存区变更,pre-push 检查 origin/main..HEAD 区间变更,两者共用同一份脚本。

门禁证明"测试存在 + 测试通过"。Red 证据由 Skill 执行层和计划验收层负责。

八、验收效果

4 个场景跑完后,项目里的测试与质量门禁状态:

场景 Red 证据 Green 证据 check:all
新增 truncateText 模块不存在 4 passed 通过
修复 copy 降级 直接抛出 Error: denied 4 passed 通过
MessageList markdown 渲染 3 个用例失败(纯文本输出) 7 passed 通过
抽取 scroll 工具 模块不存在 边界用例通过 通过

更深层的效果:

  • 需求歧义在 Red 阶段就暴露:写测试时就会发现"超长截断加省略号"到底包不包含省略号在内。
  • AI 不会顺手改多:测试只描述期望行为,AI 只写让测试通过的代码,越界改动自然变少。
  • 回归有门 :每次提交前 lint-staged 会跑 npm run test:changed,CI 再跑全量 check:all。

九、踩坑与反思

1. 触发条件按改动性质判定而不是显式触发

TDD 是否触发,不看 prompt 里有没有出现"测试""TDD"等词,只看代码改动的性质。新增功能、修 bug、改行为、重构必须走 TDD;纯样式、配置、类型、文档改动不强制单元测试。

2. 必须保留 Red 证据

口头说"我先写了测试再实现"不足信。项目要求:修 bug 时必须保留测试失败的命令输出或截图,否则不算 TDD。MessageList 那次重做就是典型案例------首轮虽实际先写了测试,但因为没单独展示红灯输出,被要求回滚实现重新跑一次。

3. Mock 用多了会测假行为

.agents/skills/test-driven-development/testing-anti-patterns.md 里专门列了反模式:

  • 不要测 mock 行为。
  • 不要为测试专门加生产方法。
  • 不要"为了保险"乱 mock。

项目目前工具函数简单,默认用真实实现;只有浏览器 API、网络、时间等边界才 mock。

十、结语

测试先行在这个项目里不是"提倡",而是一套可被机器执行的协作规则:

  • 入口规则(AGENTS.md)决定什么时候触发。
  • 专项 skill(test-driven-development/SKILL.md)决定触发后怎么做。
  • 门禁层(check-frontend-tdd)决定有没有被绕过。
  • 文档/计划层决定验收时引用什么。
  • 质量门禁(check:all)决定最终能否通过。
  • 失败证据决定是否真正先测后写。

对于 AI 参与编码的团队,这套机制的价值不在于写更多测试,而在于把"可验证"前置为默认动作。当 AI 每次改代码前都必须先回答"期望行为是什么",它离乱动就差了一步测试的距离。


项目信息 :github.com/YueJingGe/c...

相关推荐
hpoenixf2 小时前
别再把 Agent 失败都算给模型:一次从粗错误码到失败链的排查
agent
hpoenixf2 小时前
大模型有返回等于 Agent 成功运行吗?
agent
码哥字节2 小时前
Claude Code 把自己改成了任务调度器,这次设计比功能更值得看
agent·claude
杨杨杨大侠2 小时前
一句“修个 Bug”,AI 编程工具到底怎么扣额度?
人工智能·agent·ai编程
杨杨杨大侠2 小时前
MCP 到底接在了哪一层?从“Agent 调工具”说起
agent·ai编程·mcp
李溪白2 小时前
篇七:部署 —— 从本地脚本到 API 服务,再到生产环境架构选型
agent
阿里云大数据AI技术2 小时前
云栖2026 | 阿里云 OpenLake 迈向 Agentic Lake,一份全模态数据驱动智能体就绪
大数据·人工智能·agent
杨杨杨大侠2 小时前
Codex 本地自定义 Agent 与模型配置实战:TOML、AGENTS.md 和优先级
人工智能·openai·agent
阿祖zu2 小时前
只说真话,十分钟快速理解如何开始设计一个 Agent 产品
llm·agent