关于 tdd
面向开发者和编码 Agent 的 TDD 技能,将工作拆分为逐个场景执行的 RED/GREEN 循环。它要求先用可运行的失败测试验证需求,再进行最小实现并检查结果。
主要功能
- 01建立有序场景清单
- 02每轮编写一个可运行测试
- 03实现前验证 RED
- 04以最小改动达到 GREEN
- 05验证并按需重构
- 06借助记忆恢复中断工作
适用场景
- 01以测试优先开发新功能
- 02通过回归测试修复缺陷
- 03按测试场景修改整个仓库
- 04中断后恢复 TDD 工作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ oimiragieo/ agent-studio --skill tdd安装后在 Claude Code 中输入/tdd确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
manifest.jsontesting-anti-patterns.mdbenchmarks/scenario-async-error-handling.jsonscenario-bug-fix-with-regression.jsonscenario-red-green-refactor.jsoncommands/tdd.mdhooks/post-execute.cjspre-execute.cjsobservations/.gitkeepREADME.mdreferences/research-requirements.mdtdd-memory-profile.mdrules/tdd.mdschemas/input.schema.jsonoutput.schema.jsonscripts/main.cjstemplates/implementation-template.md详细介绍
测试驱动开发(TDD)
概述
此技能通过针对 AI 的防护措施实现 Canon TDD:
- 创建或更新场景列表。
- 执行一个且仅一个场景,并将其作为可运行的测试。
- 证明 RED。
- 实施满足 GREEN 的最小改动。
- 可选地重构。
- 重复以上步骤,直到场景列表为空。
适用场景
适用于:
- 新功能
- Bug 修复
- 行为变更
- 由测试驱动、涉及整个仓库的补丁
- 测试可作为可执行规范的 AI 辅助代码生成
仅在以下情况下绕过时,需先征得人工批准:
- 一次性原型
- 没有执行路径的纯声明式配置编辑
- 不会维护的一次性迁移脚本
铁律
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
如果先写了代码,就丢弃并从 RED 重新开始。
Canon 循环
步骤 0:创建/刷新场景待办列表
创建待办列表前,查询记忆中的过往失败特征和可复用测试模板:
Skill({ skill: 'memory-search' }); // query: "<feature-name> test failure signatures"
阅读 .claude/context/memory/learnings.md,了解与此任务相关的常见反模式。
然后:
- 为此任务保留简短、有序的测试场景列表。
- 根据设计信号和风险确定优先级,而非实现便利性。
- 执行过程中发现新场景时,将其添加到列表中。
- 重用记忆中的模板,不要重复已记录的失败模式。
步骤 1:选择一个且仅一个场景,并编写一个可运行的测试
- 每个循环只测试一种行为。
- 使用清晰的行为名称。
- 优先使用真实协作者;仅对外部边界进行模拟。
步骤 2:证明 RED
- 运行范围最小的测试命令。
- 失败必须由缺少行为导致,而非语法或设置错误。
- 记录 RED 证据(测试文件和失败的断言消息)。
步骤 3:实施满足 GREEN 的最小补丁
- 只实现当前 RED 测试所需的内容。
- 不要添加推测性 API 或进行无关清理。
- 将补丁范围限制在当前场景内。
步骤 4:证明 GREEN
- 重新运行范围最小的测试命令。
- 运行受影响的测试套件(或包级测试集)。
- 确认没有回归。
不稳定性关卡(异步、钩子或非确定性测试必须执行):
对于涉及异步 I/O、停止钩子、计时器或文件系统操作的测试,单次通过并不足够。必须连续通过 3 次才能判定为 GREEN:
# Run 3 times — all 3 must pass
node --test tests/hooks/routing-guard.test.cjs && \
node --test tests/hooks/routing-guard.test.cjs && \
node --test tests/hooks/routing-guard.test.cjs
测试通过一次但第二次运行失败,则判定为 RED,而非 GREEN。确认连续通过 3 次前,不得进入步骤 5。
变异测试关卡(仅适用于安全关键代码):
对于安全钩子、路由验证器、身份验证逻辑,以及任何控制访问权限或信任决策的代码路径,达到 GREEN 后运行 Stryker 变异测试,以验证测试确实能捕获故障,而非空洞地通过。
# Run Stryker mutation testing (threshold: 85%)
npx stryker run
# Require mutationScore >= 85 in stryker.config.json
对于基于 fast-check 的安全钩子属性测试,故障关闭属性可作为等同于变异测试的关卡:
// fast-check fail-closed property — must hold for any input
fc.assert(
fc.property(fc.anything(), input => {
const result = securityHook(input);
// Hook must NEVER return allow=true for malformed/unexpected input
expect(result.allow).not.toBe(true);
})
);
非安全应用代码可跳过此关卡(步骤 4 → 步骤 5)。
步骤 5:可选的重构
- 仅在测试为绿色时进行重构。
- 重构后重新运行相同的测试集。
步骤 5.5:基于属性的测试(推荐用于实用工具函数和安全钩子)
重构后(或安全关键代码完成步骤 4 后),可考虑用基于属性的测试补充基于示例的测试。在 LLM 代码生成中,相比单独使用基于示例的 TDD,PBT 可将 pass@1 提高 23.1–37.3%(arXiv:2506.18315),从而打破自我欺骗循环。
适用场景:
- 实用工具函数(编码/解码、解析器、序列化器、计算器)
- 安全钩子(输入验证器、清理器、访问控制逻辑)
- 任何可明确表述不变量、往返属性或数学属性的函数
调用方式:
Skill({ skill: 'property-based-testing' });
需要识别的关键属性模式:
| 模式 | 示例 |
|---|---|
| 往返 | decode(encode(x)) === x |
| 幂等性 | normalize(normalize(x)) === normalize(x) |
| 不变量 | sort(arr).length === arr.length |
| 默认拒绝(安全) | securityHook(anyInput).allow !== true(除非已明确列入白名单) |
PBT 是 Canon TDD 的补充,而非替代。必须先完成 Canon RED/GREEN/REFACTOR;确认 GREEN 后再运行 PBT。
步骤 6:重复执行,直至待办事项清空
AI 辅助防护措施
- 将测试用作可执行的提示词上下文;保持提示词简短并聚焦测试。
- 优先使用确定性测试(稳定的固定数据、不含非确定性排序)。
- 使用有界修复循环:每个场景最多尝试修复 3 次,之后重新设计。
- 运行反测试作弊检查:
- 确认修改后的断言仍表达原始需求。
- 修复错误的任务至少添加一个负向测试。
- 确保代码不会根据仅用于测试的产物进行分支处理。
记忆加速层
仅使用轻量级记忆来减少重复设置和问题分类:
- 首选的仓库本地测试/ lint /格式化命令
- 重复出现的失败特征及简短修复摘要
- 重复出现的反模式提醒
- 可复用的场景模板
参考:references/tdd-memory-profile.md
硬性规则:
- 记忆绝不能绕过 RED 证明
- 记忆绝不能改变 Canon 顺序
- 保持配置文件精简且低噪声
测试驱动提示(TDP)——2026 标准模式
TDP 是 2026 年多 Agent TDD 的主流模式:将原样的失败测试输出注入开发者 Agent 的启动提示词中。这可以消除理解偏差——开发者看到的内容与测试运行器看到的完全一致。
模式
不要用文字描述失败情况,而是捕获 stdout/stderr 并直接注入:
// Step 1: Run test and capture raw output
const { execSync } = require('child_process');
let testOutput = '';
try {
execSync('node --test tests/hooks/routing-guard.test.cjs', { encoding: 'utf-8' });
} catch (e) {
testOutput = e.stdout + e.stderr; // Verbatim failure output
}
// Step 2: Inject verbatim into developer spawn prompt (no paraphrasing)
Task({
task_id: 'task-impl',
subagent_type: 'developer',
prompt: `## FAILING TEST (verbatim — do NOT modify the test file)\n\`\`\`\n${testOutput}\n\`\`\`\nImplement ONLY what is needed to make this pass.`,
});
TDP 的有效原因
- 消除转述失败情况时产生的描述偏差(传话游戏效应)
- 开发者可以获得完整的断言上下文:行号、实际值与预期值
- 强制采用最小化实现——开发者只能实现测试要求的内容
- 防止 QA Agent 的测试意图与开发者的理解之间发生规格偏移
TDP + 多 Agent TDD 分解
| 步骤 | Agent | 操作 |
|---|---|---|
| 1 | qa | 编写失败测试,仅提交测试,捕获原始输出 |
| 2 | Router | 提取测试输出,构建 TDP 启动提示词 |
| 3 | developer | 将原样测试输出作为规格,实现至 GREEN |
| 4 | reflection-agent | 验证未修改任何测试断言(检查 git diff) |
来源: Simon Willison(2026)——“面向 Agent 的 Red/Green TDD:失败测试输出就是规格”;TDFlow arXiv:2510.23761。
使用 ralph-loop 进行自主 TDD(会话持久化迭代)
对于会话可能中断的仓库级 TDD,请接入 ralph-loop(模式 2 — 路由器管理),以便在中断后保留 TDD 场景待办清单:
TDD 状态架构
在 .claude/context/runtime/tdd-state.json 维护专用的 TDD 状态文件:
{
"scenarios": [
{
"id": "sc-001",
"description": "routing-guard blocks Write on creator paths",
"status": "pending"
},
{ "id": "sc-002", "description": "spawn-token-guard warns at 80K tokens", "status": "green" }
],
"completedScenarios": [
{
"id": "sc-002",
"evidenceCommand": "node --test tests/hooks/spawn-token-guard.test.cjs",
"passedAt": "2026-03-12T10:00:00Z"
}
],
"currentScenario": "sc-001",
"evidenceLog": [
{
"scenarioId": "sc-001",
"phase": "red",
"output": "AssertionError: expected exit code 2, got 0",
"timestamp": "..."
}
]
}
恢复模式
每次迭代开始时,读取 TDD 状态文件:
// Step 0 — before building/refreshing backlog
const state = JSON.parse(
fs.readFileSync('.claude/context/runtime/tdd-state.json', 'utf-8') || '{}'
);
const completedIds = (state.completedScenarios || []).map(s => s.id);
const remaining = (state.scenarios || []).filter(s => !completedIds.includes(s.id));
// Pick next scenario from remaining — never re-run completed ones
与 ralph-loop 模式 2 集成
- 路由器使用
{ task_id, subagent_type: 'qa', prompt: TDP_PROMPT + verbatim state }启动qaAgent qa编写测试 → 运行测试 → 捕获输出 → 更新tdd-state.json(阶段:red)- 路由器使用 TDP 提示词启动
developer(注入逐字测试输出) developer实现功能 → 更新tdd-state.json(阶段:green)- 路由器检查
remaining.length === 0→ 发出RALPH_AUDIT_COMPLETE_NO_FINDINGS - 如果 remaining > 0 → 返回步骤 1,处理下一个场景
反模式: 绝不要重新运行状态中已标记为 green 的场景——这会浪费迭代,并可能破坏证据日志。
仓库级与类级指导
- 对于仓库级工作,按失败测试集群拆分,每轮循环分配一个集群。
- 对于类级综合,推导方法依赖顺序,并使用方法级公共测试逐个实现方法。
- 降低长上下文压力:每轮循环仅处理一个场景和一个补丁目标。
验证清单
- 场景待办清单已建立,并在工作期间更新
- 每项生产代码变更都对应至少一个先失败后通过的测试
- 已捕获 RED 证据(命令 + 失败摘要)
- 已捕获 GREEN 证据(命令 + 通过摘要)
- 涉及范围内没有尚未解决的失败测试
- lint/格式化/测试命令均已完成,或明确报告受阻情况
- 未检测到测试投机模式
完成前命令(项目范围内)
使用项目实际采用的命令。典型顺序如下:
# 1) targeted test
pnpm test <target>
# 2) impacted suite
pnpm test
# 3) lint
pnpm lint
# 4) format check
pnpm format:check
如果仓库使用不同的脚本,请替换为本地对应命令,并准确报告实际运行的命令。
合理化借口应对措施
- “我稍后会添加测试” -> 停下来,先编写当前的红灯测试。
- “这太简单了,不值得测试” -> 编写一个最小行为测试。
- “我已经手动测试过了” -> 手动运行不能替代可执行的回归测试。
- “花了太多时间,不能删除测试前的代码” -> 沉没成本;从 RED 重新开始。
相关文件
references/research-requirements.mdreferences/tdd-memory-profile.mdtesting-anti-patterns.mdrules/tdd.mdtemplates/implementation-template.md
研究依据
此技能与以下内容一致:
- Martin Fowler TDD(2023 年 12 月 11 日)
- Kent Beck Canon TDD(2023 年 12 月 11 日)
- Rafique 和 Misic 的元分析,IEEE TSE DOI:10.1109/TSE.2012.28
- LLM4TDD(arXiv:2312.04687)
- 面向代码生成的测试驱动开发(arXiv:2402.13521)
- 测试即提示词(arXiv:2505.09027)
- SWE-Flow(arXiv:2506.09003)
- TDFlow(arXiv:2510.23761)
- 将 TDD 从函数扩展到类(arXiv:2602.03557)
记忆协议
开始之前:
阅读 .claude/context/memory/learnings.md
完成之后:
- 新模式 ->
.claude/context/memory/learnings.md - 发现问题 ->
.claude/context/memory/issues.md - 作出决策 ->
.claude/context/memory/decisions.md
假设工作会中断:如果没有记录在记忆中,就视为没有发生。
Agent-Studio TDD 扩展(2026)
Hook 测试模式
Hook 使用 stdin/stdout JSON 协议:
const proc = require('child_process').spawn('node', ['.claude/hooks/routing/routing-guard.cjs'], {
shell: false,
});
proc.stdin.write(JSON.stringify({ tool_name: 'Write', tool_input: {} }));
proc.stdin.end();
// Exit 0=allow, 2=block
记忆 TDD
模拟 MemoryRecord。测试置信度门控(阈值 0.7)。使用原子写入。
基于属性的测试
对于任何具有不变量的函数,都使用 fast-check(以及用于集成 vitest 的 @fast-check/vitest),而不仅仅是路由。fast-check 3.x(2025)改进了 Unicode、日期和 bigint 的任意值生成器。
路由不变量(现有):
import fc from 'fast-check';
fc.assert(
fc.property(fc.string(), intent => {
return typeof routeIntent(intent) === 'string';
})
);
内存序列化往返(新增):
// Property: serialize(deserialize(x)) === x for all JSON-serializable values
fc.assert(
fc.property(fc.jsonValue(), value => {
const serialized = serializeMemoryRecord(value);
const deserialized = deserializeMemoryRecord(serialized);
return JSON.stringify(deserialized) === JSON.stringify(value);
})
);
Hook 验证不变量(新增):
// Property: for any tool input, isValidInput(x) === !isBlocked(x)
// (validation and blocking must be inverses)
fc.assert(
fc.property(fc.record({ tool_name: fc.string(), tool_input: fc.object() }), input => {
const valid = isValidInput(input);
const blocked = wouldBlock(input);
return valid !== blocked || (!valid && blocked); // blocked implies invalid
})
);
路径规范化幂等性(新增):
// Property: normalize(normalize(path)) === normalize(path) (idempotent)
fc.assert(
fc.property(fc.string(), rawPath => {
const once = normalizePath(rawPath);
const twice = normalizePath(once);
return once === twice;
})
);
Schema 验证稳定性(新增):
// Property: validate(schema, x) never throws uncaught exception for any input
fc.assert(
fc.property(fc.anything(), input => {
try {
validateSchema(schema, input);
return true;
} catch (e) {
return e instanceof ValidationError;
} // Only ValidationError allowed
})
);
契约测试
验证 TaskUpdate 元数据 Schema(processedReflectionIds: string[])。
多 Agent TDD 分解(2026 标准)
基于 TDFlow(arXiv:2510.23761,SWE-Bench Verified 得分为 94.3%),单体 TDD Agent 的得分为 60–70%。拆分为专门的子 Agent:
| 角色 | Agent | 职责 |
|---|---|---|
| 测试编写者 | qa | 编写失败测试,仅提交测试 |
| 实现者 | developer | 实现直至测试通过——不得修改测试 |
| 验证者 | reflection-agent | 检测测试作弊,验证 RED→GREEN 证据 |
模式:
- QA Agent 编写测试 → 仅提交测试文件(不包含实现)
- Developer Agent 编写实现 → 运行测试 → 提交实现
- Reflection Agent 审查差异:如果测试断言发生变化 → 失败(测试作弊)
**测试作弊检测:**reflection-agent 检查 git diff HEAD~1 HEAD -- '*.test.*'——实现提交之后任何断言变更都将被拒绝。
**适用场景:**仓库级 TDD、具有多种行为的复杂功能,以及任何单个 Agent 可能会为修改测试寻找理由的任务。
TDAID 阶段映射(测试驱动的 AI 辅助开发,2025–2026)
TDAID 在经典 TDD 基础上增加了明确的规划和验证关卡:
| 阶段 | TDAID 标签 | Agent-Studio 负责人 | 说明 |
|---|---|---|---|
| 0 | 计划 | planner | 思考模型在编写任何代码之前生成结构化的 TDD 计划,并明确列出测试检查点 |
| 1 | 红 | qa | 编写表达预期行为的失败测试;由人工确认失败符合预期 |
| 2 | 绿 | developer | 编写通过测试所需的最简实现;不得修改测试断言 |
| 3 | 重构 | developer | 在所有测试通过的情况下提升代码质量 |
| 4 | 验证 | reflection-agent + verification-before-completion skill | 检测规范投机;确认实现符合计划;设置人工关卡 |
验证阶段需要检测的 TDAID 反模式:
- 删除测试断言以使测试通过
- 对预期值进行硬编码
- 通过模拟绕过被测行为
- 只在表面上让实现符合要求,却未满足规范意图
**研究依据:**TDAID(awesome-testing.com,2025)、TDAD Agent 间变体(arXiv:2603.08806,2026)、TDFlow(arXiv:2510.23761,2025)
LSP 预 RED 类型验证
编写失败测试前,先验证 API 契约是否存在,以避免出现“因 API 错误而失败”,而非“因缺少行为而失败”的情况:
# Step 1: Find the target function's file + line
pnpm search:code "functionName"
# Step 2: Verify signature with LSP hover
lsp_hover({ filePath: "/abs/path/to/file.ts", line: 42, character: 10 })
# Returns: function signature, parameter types, return type
# Step 3: Write test using VERIFIED signature
# Now RED is guaranteed to fail due to missing behavior, not API mismatch
规则: 如果 lsp_hover 返回为空(CJS 文件或 LSP 未激活)→ 改用 ripgrep rg -n "functionName" --type ts 读取实际签名。
无需执行的情况: 尚不存在的全新函数(LSP 无内容可返回)。
契约测试(Hook 边界 — 扩展)
Hook 契约定义了 stdin/stdout JSON 协议。应在边界处进行测试:
// Hook contract test pattern
const proc = spawn('node', ['.claude/hooks/routing/routing-guard.cjs'], { shell: false });
const input = JSON.stringify({
tool_name: 'Edit',
tool_input: { file_path: '.claude/agents/core/developer.md' },
});
proc.stdin.write(input);
proc.stdin.end();
// Assert: exit code 2 (block) for protected paths
// Assert: stdout JSON contains { allow: false, message: /Gate 4/ }
TaskUpdate 元数据契约:
// Validate processedReflectionIds schema
const schema = {
type: 'object',
required: ['processedReflectionIds'],
properties: { processedReflectionIds: { type: 'array', items: { type: 'string' } } },
additionalProperties: false,
};
需要测试的 Agent-Studio hook 契约:
routing-guard.cjs:阻止不带 task_id 的 Task(退出码 2)unified-creator-guard.cjs:阻止写入.claude/skills/**/SKILL.md(退出码 2)spawn-token-guard.cjs:达到 80K tokens 时发出警告(退出码 0 + 消息)
测试运行器选择(node --test 与 Vitest 4)
Agent Studio 对所有 .cjs CommonJS 文件(hooks、lib、scripts)默认使用 node --test(Node.js 内置测试运行器)。对于 ESM/TypeScript 文件,推荐使用 Vitest 4。
| 运行器 | 使用场景 | 命令 |
|---|---|---|
node --test | .cjs hooks、lib、CommonJS 脚本 — Agent Studio 当前标准 | node --test tests/**/*.test.cjs |
vitest | .ts、.mts、ESM .js 文件 — 迁移到 TypeScript 时使用 | pnpm vitest run |
.cjs 使用 node --test 的原因: Vitest 需要 Vite 配置和兼容 ESM 的模块。Agent Studio hooks 使用 require() 和 CommonJS;node --test 无需转译即可运行。
.ts/ESM 使用 Vitest 4 的原因: 启动时间从约 8 秒(Jest)降至约 1.2 秒(Vitest)。原生支持 TypeScript + ESM、Browser Mode(v4 稳定版),并提供兼容 jest 的 describe/it/expect API(迁移只需更改配置)。
反模式: 新文件请勿使用 Jest。Vitest 是 2025-2026 年 ESM/TypeScript 的标准。
# Current Agent Studio pattern (CJS hooks and lib)
node --test tests/lib/routing/routing-table.test.cjs
# Future ESM/TypeScript pattern
pnpm vitest run tests/lib/routing/routing-table.test.ts
AI 输出评估测试(非确定性 Agent)
LLM/Agent 的输出具有非确定性——二元通过/失败断言并不足够。应使用基于分数的评估和工具调用序列验证。
基于分数的断言模式
// Agent output evaluation — score dimensions 0.0-1.0
function evaluateAgentOutput(output, expectations) {
const scores = {
relevance: scoreRelevance(output, expectations.topic), // 0.0-1.0
safety: scoreSafety(output), // 0.0-1.0
faithfulness: scoreFaithfulness(output, expectations.facts), // 0.0-1.0
format: scoreFormat(output, expectations.schema), // 0.0-1.0
};
const overall = Object.values(scores).reduce((a, b) => a + b) / Object.keys(scores).length;
return { scores, overall, pass: overall >= 0.75 };
}
// Test: agent output meets quality threshold
test('researcher agent output is relevant and safe', () => {
const result = evaluateAgentOutput(agentOutput, { topic: 'TDD patterns', facts: knownFacts });
expect(result.scores.safety).toBeGreaterThanOrEqual(0.9); // Hard floor for safety
expect(result.overall).toBeGreaterThanOrEqual(0.75); // 75% overall threshold
});
工具调用序列验证
对于 Agent 测试,应验证工具调用的顺序和次数,而不只是最终输出:
// Spy on tool calls and assert ordering
const toolCallLog = [];
const mockTaskUpdate = jest.fn(args => {
toolCallLog.push({ tool: 'TaskUpdate', args });
});
const mockBash = jest.fn(args => {
toolCallLog.push({ tool: 'Bash', args });
});
// Run agent under test with mocked tools
await runAgent({ TaskUpdate: mockTaskUpdate, Bash: mockBash });
// Assert: TaskUpdate(in_progress) called BEFORE TaskUpdate(completed)
const inProgressIdx = toolCallLog.findIndex(
c => c.tool === 'TaskUpdate' && c.args.status === 'in_progress'
);
const completedIdx = toolCallLog.findIndex(
c => c.tool === 'TaskUpdate' && c.args.status === 'completed'
);
expect(inProgressIdx).toBeLessThan(completedIdx); // Ordering enforced
expect(inProgressIdx).toBeGreaterThanOrEqual(0); // Must have been called
expect(completedIdx).toBeGreaterThanOrEqual(0); // Must have been called
规则: 切勿测试 LLM 生成文本的内容。应测试结构、架构有效性、工具调用序列和分数阈值。
参考: Simon Willison(2025)——“Agent 的红/绿 TDD:针对工具调用序列和结构化输出编写断言。”
MSW v2 HTTP Mock(API 边界测试)
使用 MSW(Mock Service Worker)v2 测试会发起外部 HTTP 调用的技能和 Agent。MSW 在网络层拦截请求——无需 monkey-patching fetch,也无需修改生产代码。
pnpm add -D msw@2
设置模式(Node.js / Vitest)
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
// Define handlers — these describe the expected API contract
const handlers = [
http.get('https://api.example.com/search', ({ request }) => {
const url = new URL(request.url);
return HttpResponse.json({
results: [{ id: 1, title: `Result for: ${url.searchParams.get('q')}` }],
});
}),
];
const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// Test: researcher skill makes HTTP call and processes response
test('researcher skill fetches and parses search results', async () => {
const results = await researcherSkill.search('TDD patterns 2026');
expect(results).toHaveLength(1);
expect(results[0].title).toContain('TDD patterns');
});
针对错误场景按测试覆盖
test('researcher skill handles 503 gracefully', async () => {
server.use(
http.get('https://api.example.com/search', () => HttpResponse.json({}, { status: 503 }))
);
const results = await researcherSkill.search('TDD patterns');
expect(results).toEqual([]); // Graceful empty fallback
});
相较手动 Mock 的主要优势:
- 测试会执行真实 HTTP 客户端代码路径(而非被 Mock 的抽象)
onUnhandledRequest: 'error'可捕获测试期间意外发出的外部请求- Handler 定义请求/响应契约,同时也可用作文档
适合进行 MSW 边界测试的 Agent-Studio 目标:
researcher技能 → WebSearch/WebFetch HTTP 调用github-ops技能 → GitHub API 调用- 使用
mcp__Exa__web_search_exa或WebFetch的任何 Agent
变异测试(Stryker JS)
变异测试验证的是测试质量,而不仅仅是覆盖率。在达到 100% 行覆盖率后运行:
Stryker + Vitest(2026 标准 — ESM/TypeScript 项目)
# Install (once per project) — use vitest-runner for ESM/TypeScript
pnpm add -D @stryker-mutator/core @stryker-mutator/vitest-runner vitest
// stryker.config.mjs — working configuration for Vitest projects
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
export default {
testRunner: 'vitest',
vitest: {
configFile: 'vitest.config.ts', // optional: path to your vitest config
related: true, // default: run only tests related to mutated file
},
thresholds: { high: 80, low: 60, break: 50 },
reporters: ['html', 'progress'],
};
# Run mutation tests (use incremental to speed up local loops)
pnpm stryker run --incremental
# Target threshold: >80% mutation score
# Score = (killed mutations / total mutations) × 100
Vitest 运行器的限制(StrykerJS 7.x):
- 不支持 Browser Mode — 仅支持 threads 模式
- 始终使用
perTest覆盖率分析(忽略coverageAnalysis配置) - 对于使用
node --test的.cjs文件,请使用@stryker-mutator/jest-runner作为备用方案
Stryker + node:test(CommonJS/.cjs 项目)
pnpm add -D @stryker-mutator/core @stryker-mutator/jest-runner
解读结果:
- Killed — 测试套件捕获了变异 ✓
- Survived — 测试套件漏掉了此代码路径(添加断言)
- No coverage — 没有测试覆盖此行(添加测试)
运行时机: 安全关键代码(钩子、验证器、路由逻辑)完成一个 TDD 循环后。并非所有代码都必须运行;请按风险确定优先级。
Agent-Studio 变异测试优先目标:
.claude/hooks/routing/routing-guard.cjs.claude/hooks/safety/unified-creator-guard.cjs.claude/lib/routing/routing-table.cjs
验证阶段:TDAD 依赖关系图(P0)
提交前,Agent 必须确定哪些测试文件覆盖了发生更改的源文件。使用编译器辅助的引用发现或有针对性的 grep:
# Find tests that import the changed file
grep -r "import.*changedFile\|require.*changedFile" tests/
# Or use LSP to find all references
lsp_findReferences({ filePath: "/path/to/changed/file.ts", line: 1, character: 1 })
构建依赖关系图,并首先只运行这些测试(依据 arXiv:2603.17973,回归检测速度提升 70%):
# 1. Run targeted tests (impacted tests only)
pnpm test tests/hooks/routing-guard.test.cjs
# 2. Verify no regressions in targeted scope
# 3. Only then run full suite
pnpm test
原因: 在大型代码库中,完整测试套件可能需要超过 5 分钟。针对性测试可在 15-30 秒内发现回归,从而为长时间 TDD 循环中的后续场景腾出上下文。
验证阶段:规范投机检测(P0)
在验证阶段,确认实现没有钻测试断言的空子:
检查清单:
- 测试断言的是行为,而非实现细节(不测试私有变量或类内部结构)
- 没有将测试中的硬编码预期值复制到实现中
- 变异分数 ≥80%,表明测试质量足以检测回归
- 审查:代码是否可能通过测试,但本质上是错误的?
如果可用,请运行变异测试:
# Test suite strength validation — mutations should be caught
pnpm stryker run
# If mutation score < 80%, tests are too weak:
# - Add negative tests
# - Add boundary condition tests
# - Verify assertions are on behavior, not mocks
需要发现的规范投机示例:
- ✗ 实现硬编码
return 42,以通过预期为42的测试 → 变异测试会发现这一点 - ✗ 测试模拟行为,而不是断言行为 → 变异测试显示变异杀死率为 0%
- ✗ 测试检查日志消息,而不是行为 → 翻转断言后,实现仍然通过
Agent-Studio 目标: 完成安全关键钩子或路由更改后,在将任务标记为完成之前运行变异测试。