mcpField
T
tdd

tdd

github.com/oimiragieo测试与质量4466 次安装17 周前更新
代码仓库

关于 tdd

面向开发者和编码 Agent 的 TDD 技能,将工作拆分为逐个场景执行的 RED/GREEN 循环。它要求先用可运行的失败测试验证需求,再进行最小实现并检查结果。

主要功能

  1. 01建立有序场景清单
  2. 02每轮编写一个可运行测试
  3. 03实现前验证 RED
  4. 04以最小改动达到 GREEN
  5. 05验证并按需重构
  6. 06借助记忆恢复中断工作

适用场景

  1. 01以测试优先开发新功能
  2. 02通过回归测试修复缺陷
  3. 03按测试场景修改整个仓库
  4. 04中断后恢复 TDD 工作

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/oimiragieo/agent-studio --skill tdd

安装后在 Claude Code 中输入/tdd确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

manifest.json
testing-anti-patterns.md
benchmarks/
scenario-async-error-handling.json
scenario-bug-fix-with-regression.json
scenario-red-green-refactor.json
commands/
tdd.md
hooks/
post-execute.cjs
pre-execute.cjs
observations/
.gitkeep
README.md
references/
research-requirements.md
tdd-memory-profile.md
rules/
tdd.md
schemas/
input.schema.json
output.schema.json
scripts/
main.cjs
templates/
implementation-template.md

详细介绍

测试驱动开发(TDD)

概述

此技能通过针对 AI 的防护措施实现 Canon TDD:

  1. 创建或更新场景列表。
  2. 执行一个且仅一个场景,并将其作为可运行的测试。
  3. 证明 RED。
  4. 实施满足 GREEN 的最小改动。
  5. 可选地重构。
  6. 重复以上步骤,直到场景列表为空。

适用场景

适用于:

  • 新功能
  • 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操作
1qa编写失败测试,仅提交测试,捕获原始输出
2Router提取测试输出,构建 TDP 启动提示词
3developer将原样测试输出作为规格,实现至 GREEN
4reflection-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 集成
  1. 路由器使用 { task_id, subagent_type: 'qa', prompt: TDP_PROMPT + verbatim state } 启动 qa Agent
  2. qa 编写测试 → 运行测试 → 捕获输出 → 更新 tdd-state.json(阶段:red)
  3. 路由器使用 TDP 提示词启动 developer(注入逐字测试输出)
  4. developer 实现功能 → 更新 tdd-state.json(阶段:green)
  5. 路由器检查 remaining.length === 0 → 发出 RALPH_AUDIT_COMPLETE_NO_FINDINGS
  6. 如果 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.md
  • references/tdd-memory-profile.md
  • testing-anti-patterns.md
  • rules/tdd.md
  • templates/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 证据

模式:

  1. QA Agent 编写测试 → 仅提交测试文件(不包含实现)
  2. Developer Agent 编写实现 → 运行测试 → 提交实现
  3. 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 目标: 完成安全关键钩子或路由更改后,在将任务标记为完成之前运行变异测试。

评论

支持 Markdown