mcpField
T
test-design

test-design

github.com/gusen1453测试与质量336 次安装6 周前更新
代码仓库

关于 test-design

test-design 是指导测试用例设计的 Agent skill,面向编写或审查单元与集成测试的开发者。它聚焦用户可观察的行为契约,并以变异检查验证测试是否能发现回归。

主要功能

  1. 01围绕行为契约设计测试
  2. 02按等价类与边界选取用例
  3. 03断言返回值、状态和副作用
  4. 04限制模拟对象并优先使用 Fake
  5. 05通过变异检查验证测试有效性

适用场景

  1. 01为新功能规划单元或集成测试
  2. 02审查测试是否只覆盖实现细节
  3. 03选择边界值和脏输入用例

安装

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

$npx skills add https://github.com/gusen1453/creed --skill test-design

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

详细介绍

测试设计

**输出语言:**使用用户自己的语言回答并编写产物(与用户的消息保持一致,而不是与仓库的历史保持一致)。

概述

回答**“测试应该输入什么、断言什么,以及模拟什么”**。不涉及红—绿—重构节奏(那是 TDD)。

核心原则:测试保护的是用户/调用方可观察到的行为契约,而不是实现细节,更不是覆盖率数字。 好的测试是对契约可证伪的规范;坏的测试只是用另一种语法复述代码。

**必备配套:**关于先写测试的节奏,请参阅 tdd。关于模拟对象增多时的模块边界,请参阅 solid。此技能只规范用例和断言的设计质量。

适用场景

  • 为一段代码添加或修改单元/集成测试
  • 审查他人的测试,判断它们是否有效,还是流于形式
  • 决定“这段代码值得测试吗?”或“该用单元测试还是集成测试?”
  • 因覆盖率压力、截止日期紧迫或沉没成本而准备编写无效测试

**不适用场景:**仅运行测试/查看失败 → test-runner。仅遵循测试优先的节奏 → tdd。

铁律(不可妥协)

Break one line of production code and the test protecting it MUST turn red.
If it can't, the test is formalism — delete it and rewrite.

写完每个测试后,在脑中(或实际)破坏它所保护的逻辑,并确认测试会变红。 违反规则字面要求,就是违背规则精神——“我的覆盖率没问题”和“我遵循了规则精神”都不算数。

工作流(调用时)

宣布“正在使用 test-design 来……”,然后为每个步骤创建一个待办事项:

  1. 判断是否值得测试(§1):自身逻辑复杂 → 单元测试;依赖关系接线 → 集成测试。不值得 → 不要写。
  2. 列出行为清单(§2):只依据契约——“当……时,应该……”,包括没有专门分支的情况(例如“不要删除我的合法内容”)。每个用例都要用面向读者的语言写明其目标/条件/参数(见 §2)。
  3. 选择代表点(§3):等价类/边界/决策表/状态转换/脏输入——每类选一个代表,不要添加同质的重复用例。
  4. 编写断言(§4):只断言返回值/可观察状态/副作用边界;范围要窄且明确;手动编写 expected。
  5. 决定模拟对象(§5):只模拟外部世界;数量 ≤ 2-3;优先使用 Fake/Recording。
  6. 变异检查(铁律):破坏生产代码,确认每个测试都会变红。
  7. 检查合理化对照表和危险信号;如命中任何一项,立即停下并重新思考。

§1 是否值得测试(投资回报率,而非覆盖率)

判断标准:复杂性是在代码自身逻辑中,还是在依赖项之间的交互中?

  • 自身逻辑(算法、纯计算、领域模型/状态机、分支繁多的策略/验证/降级逻辑)→ 单元测试,投资回报率最高。
  • 依赖关系接线(CRUD、A.get() → convert() → B.save() 胶水代码)→ 集成测试。此类代码的风险恰恰存在于模拟对象取代真实依赖的地方(依赖项返回 null、超时、抛出异常、事务未提交);单元测试只证明“在一切顺利时它能运行”=毫无价值。

不要对胶水代码做单元测试;不要对算法做集成测试。

§2 根据行为列表查找用例

仅根据契约编写“当……时,应……”。绝不要统计源码中的 if。 列表 MUST 包含没有专门分支、但用户最关心的事项。 如果某个用例是“读源码时写出来的”,就删除并重写——它保护的是实现,而不是契约。

每个用例都必须让非作者看得懂。 在任何代码之前,用通俗的用户/产品语言说明以下三点:

  • 目标 — 用一句非作者能理解的话说明该用例要证明什么(用户可见的结果,而非实现机制)。
  • 条件 — 用例设置的状态(谁/哪些数据/刚刚发生了什么)。
  • 参数 — 具体输入和预期的可观察结果(取值,以及为什么这个值值得关注——边界值、极端值、真实场景)。

如果一个用例的目标只有重读源码才能看懂,就没有达到要求。示例:

Case: re-import with the same file twice
- Goal:      a re-run must not double-count rows (the bug users reported as "my totals doubled")
- Conditions: the file was already imported once; no other change
- Parameters: same file; expect the row count unchanged, and an idempotency marker set

§3 选择代表性取值的五种技巧

  1. 等价类划分:每个行为组选择一个代表值(同一组中的第二个值 = 重复)。
  2. 边界值:每个阈值都测试 boundary-1 / boundary / boundary+1(>=4 必须测试“恰好 3”和“恰好 4”——只有这样才能发现 > 与 >= 的区别)。
  3. 决策表:当输出取决于多个条件时,枚举各种组合;必须包含“多个条件同时命中”;条件很多时使用两两组合测试。
  4. 状态转换:合法路径 + 非法转换(关闭后使用)+ 幂等性(关闭×2)+ 重复操作。
  5. 异常/恶意输入:空值、null、超大值、巨型 JSON、结构损坏(缺少闭合标签、嵌套)、类型错误。

§4 断言(正向做法)

仅断言以下三种可观察目标:① 返回值/抛出的错误(类型 + 消息 + 代码)② 可观察的状态变更 ③ 副作用边界(是否发送请求、发送到哪里、参数结构)。

  • 范围窄且要求严格:多个精确的小断言 > 一个大型 toEqual(whole object)。
  • 手写 expected:expected = input.replace(sameRegex) 是在断言“正则表达式与自身相同”——永远通过。
  • 不要触及实现细节:私有调用次数/内部分支,除非“次数/顺序”本身就是契约(例如,恰好重试 3 次、不得重复扣费)。

§5 Mock 规则

只替换缓慢/不稳定/有副作用/成本高昂的外部协作者。不要用它来绕过难以测试的设计。

应 Mock绝不要 Mock
网络/HTTP/第三方 SDK/LLM、DB/FS/MQ、时钟、随机数生成器被测单元、纯函数、值对象、模式、模块内的简单协作者

保真度顺序:Fake(内存中)> Recording/contract replay > Spy > Mock 框架。只断言契约结构:

// Good: assert the outward contract
expect(fetchMock).toHaveBeenCalledWith(
  expect.objectContaining({ method: "POST", url: expect.stringContaining("/nodes") }),
)
// Bad: lock implementation detail, red on any refactor
expect(buildPayload).toHaveBeenCalledTimes(1)

§6 用户视角

从真实使用者的视角断言:终端用户看到结果;上游 API/Agent 看到字段和错误代码;运维人员能看到日志/状态是否有助于定位问题。 测试名称 = 角色 + 场景 + 预期(禁止使用 case 1/should work);从公共入口进入,不要深入私有函数。

§7 选择测试结构

逻辑密集型 → 金字塔(底层有大量单元测试);业务/CRUD → 菱形(中间有大量集成测试,单元测试仅用于工具函数和核心算法)。 集成测试能捕获单元测试无法发现的问题:事务提交、JSON 字段丢失、参数校验、真实依赖故障。

合理化借口表(写形式化内容前如何自欺欺人)

借口现实
“加个测试覆盖这个分支就行”覆盖率不是保护。问问“用户会遇到什么问题?”;没有风险 → 不要测试。
“用函数计算 expected 更准确”这相当于测试“实现等于它自身”——永远通过。expected 必须是手写的字面值。
“再多模拟一点,隔离得更干净”模拟一大堆依赖 = 拆分代码或改做集成测试;你用假实现替换了最容易出错的部分。
“不如也把这些胶水代码/CRUD 测试一下”其风险完全在被模拟掉的依赖中;使用集成测试。
“测一个边界就够了”只有在边界处才能捕获差一错误。
“测这几个用例更全面”同一等价类中的多个用例都是同质重复 = 形式主义。
“快要发布了,验证调用就够了”verify(called once) 什么也保护不了。时间紧 → 少写一些,而不是弄虚作假。
“给整个对象做快照最简单”任何无关字段变动都会导致脆弱的测试失败,还会掩盖具体哪个契约出了问题。
“这个测试很难写,绕着它模拟一下”难以测试 = 难以使用——这是设计信号;修复代码。
“看起来专业 / 要求达到 80%”100 个照抄实现的测试什么也保护不了。看质量,不看数量。

危险信号——立即停止

  • 只是为了点亮某一行/分支而添加测试
  • expected 用生产逻辑计算,而不是手写
  • 一个“单元测试”模拟了 3 个或更多依赖
  • 断言全是 verify(...) / toHaveBeenCalled,没有断言返回值/状态
  • 测试名称像 test1 / should work / case 2
  • 测试 getter/setter/DTO 转换/纯胶水代码
  • “大概把代码弄坏了,这个测试也不会变红”

命中任何一项 → 停止,回到这个问题:“这个测试要防止什么用户可见的不良结果?”答不上来 → 不要写。

演示示例(串联使用多种技巧)

被测对象:strip(prompt) 会移除证据摘录并保留简短指令;包含 ≥4 个 [n] 引用的段落会被丢弃。

行为清单 → 用例(不是跟着分支走):

// role+scenario+expectation; narrow/hard assertions; hand-written expected
test("subagent receiving a prompt with task_result has the dump stripped but instructions kept", () => {
  const out = new HeuristicDumpStripper().strip(
    "Summarize in three sentences.\n<task_result>…huge dump…</task_result>\nPrefer the latest conclusion.",
  )
  expect(out).not.toContain("<task_result>")        // dump gone
  expect(out).toContain("Summarize in three sentences")  // leading instruction kept
  expect(out).toContain("Prefer the latest conclusion")  // trailing instruction kept
})

// Boundary values: threshold >=4, test "exactly 3 kept / exactly 4 dropped"
test("paragraph with exactly 3 citations is kept", () => {
  expect(strip("conclusion [1][2][3] still body")).toContain("still body")
})
test("paragraph with exactly 4 citations is dropped", () => {
  expect(strip("noise [1][2][3][4] should be removed")).not.toContain("should be removed")
})

// Dirty input + fallback: non-empty when everything is stripped
test("falls back to truncated original (non-empty) when stripping empties it", () => {
  expect(strip("<task_result>only dump</task_result>").length).toBeGreaterThan(0)
})

**变异检查:**将 hits.length >= 4 改为 >= 5;“恰好 4 个时被丢弃”必须变红。如果全部通过 → 阈值未经过测试 → 重写。

检查清单(每个测试运行一次)

  • 值得测试吗?复杂逻辑 → 单元测试;胶水代码 → 集成测试
  • 用例来自行为清单,而不是从源码分支照搬
  • 每个等价类选一个代表;每个阈值覆盖边界值±1;覆盖“多个条件同时命中”
  • 空输入/超大输入/错误输入/恶意输入各测一个用例
  • 只断言返回值/可观察状态/副作用;范围小且难以伪造;expected 手写
  • 只模拟外部 IO/时间/RNG;单元逻辑保持真实;模拟数量 ≤ 2-3
  • 测试名称 = 角色+场景+预期;通过公共 API 进入
  • 变异检查通过:破坏生产代码后,这个测试会变红

评论

支持 Markdown