Files
EapilSkillMarket/plugins/codex/plugins/superpowers-zh/skills/test-driven-development/writing-good-tests.md
T
KeyInfo Bot c09512f26a Sync third-party and MCP marketplace plugins
Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources.
Confidence: high
Scope-risk: narrow
Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history.
Tested: Marketplace validation passed.
2026-10-27 15:58:23 +08:00

8.2 KiB
Raw Blame History

编写高质量测试

在以下情况加载此参考: 编写或修改测试、添加 mock,或添加仅供测试使用的清理/辅助方法。

概述

测试存在的意义,是捕获一种具体的破坏。这里的一切都由两个原则支配:

1. 每个测试都指出它能捕获的破坏
2. 每个测试都验证真实事物

严格的 TDD 会自然地产生这两点:先编写测试,并亲眼看到它针对真实代码失败, 就已经证明该测试确实能失败;只有当真实依赖已被证明缓慢或位于外部时,才允许使用 mock。

原则 1:指出破坏

编写测试主体之前,先回答:生产代码中的哪项变更应该让这个测试失败—— 而该变更是缺陷,还是有意决策? 一个测试只有在能捕获错误分支、缺失的副作用、 错误参数、边界情况或已破坏契约时,才有存在价值。

独立推导期望值。 使用字面量和人工核对过的夹具;带有字面量 want 值的 表驱动测试是首选形式。若期望值由被测代码或其辅助函数计算,那么无论被测代码 做什么,测试都会通过:

// ❌ Mirror assertion: the same builder computes both sides — always true
const expected = buildSearchQuery({ tag: 'urgent' });
expect(buildSearchQuery({ tag: 'urgent' })).toBe(expected);

// ✅ Hand-derived literal
expect(buildSearchQuery({ tag: 'urgent' })).toBe('tag:"urgent"');

拒绝变更检测器(变更检测器陷阱)。 如果只有有意决策才能让测试失败—— 例如常量值、消息的精确措辞或私有结构——它会在重新设计时报警,却对缺陷沉睡。 请测试依赖该决策的行为:不要写 expect(MAX_RETRIES).toBe(5),而要验证 “失败的调用会重试 5 次,且绝不会进行第 6 次尝试”。

验证行为,而不是文本(字符串存在性陷阱)。 断言脚本、技能或配置中包含 某一精确行,只能证明源文件里确实有这行文字。请用受控输入运行脚本,并断言输出、 副作用或退出码。指导 Agent 的文档应通过使用该文档的 Agent 行为来测试 (superpowers:writing-skills);面向人类的说明文字完全不需要测试。

测试你的代码,而不是框架。 测试你的代码在边界处承诺的契约——注册的路由、 发出的查询、生成的载荷。上游机制应由它们的维护者编写测试(经典例子:断言路由器 调用已注册的处理器——那是框架的测试,不是你的)。如果上游行为确实令你意外, 请编写一个狭窄的特征测试,明确指出该假设。同样的边界也适用于代码内部:构造函数、 getter、常量和简单转发,只有在执行验证、规范化、默认值、派生、约束或副作用时才值得 单独测试——否则,请断言第一个依赖它们、且消费者可观察到的结果。

门禁函数

编写测试主体之前:
  指出哪项生产代码变更会使此测试失败。

  无法指出                 → 围绕可观察行为重新设计
  “源文本发生了变化”       → 运行该产物并断言其效果
  只有有意决策会导致失败   → 变更检测器;测试依赖该决策的行为

  确认期望值的推导没有使用被测代码。
  如果它复用了被测代码的逻辑或辅助函数:
    替换为字面量或人工核对过的夹具

原则 2:验证真实事物

mock 不值得拥有断言。 有 mock 时通过、没有 mock 时失败的断言,完全没有说明 组件的行为。断言真实组件的行为;如果你检查的是 mock,请取消 mock 或删除该断言。

// ✅ Real behavior
expect(screen.getByRole('navigation')).toBeInTheDocument();

// ❌ Mock existence
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();

你的人类伙伴的纠正: “我们是在测试一个 mock 的行为吗?”

在正确层级进行 mock。 替换真实方法之前,先了解它的每一项副作用;mock 缓慢 或外部的操作,同时保留测试所依赖的真实行为。如果不确定,请先针对真实实现运行测试, 观察究竟需要发生什么。

// ❌ The mock swallows the config write that duplicate detection reads
vi.mock('ToolCatalog', () => ({
  discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
}));

// ✅ Mock only the slow server startup; the config write stays real
vi.mock('MCPServerManager');

让测试替身具体明确。 当参数、调用次数或顺序属于契约的一部分时,请断言它们—— 接受任何内容的 fake 什么也验证不了。为每个分支(成功、错误、格式错误)分别提供 独立夹具或 spy,这样错误分支就无法满足期望。

完整镜像真实数据。 按照真实存在的完整结构——包括所有已记录字段——构造 mock, 而不是只提供测试读取的字段。下游代码读取被省略字段时,部分 mock 会悄悄失效: 测试通过了,集成却坏了。

生产类只承载生产方法。 只有测试需要的清理逻辑应放在测试工具中,绝不能作为 生产类上的 destroy()。请问:这个方法是否只从测试中调用?这个类是否拥有该资源的 生命周期?答案不正确 → 放入测试工具。

复杂 mock 应让位于真实组件。 当 mock 设置比测试逻辑还长、mock 缺少真实组件 拥有的方法,或 mock 一变测试就坏时,请切换到使用真实组件的集成测试。 你的人类伙伴的问题: “这里真的需要使用 mock 吗?”

门禁函数

添加 mock 或测试辅助函数之前:
  列出真实方法的副作用;让测试依赖的副作用保持真实——
  只 mock 它们下层缓慢或外部的部分。

  mock 响应应完整镜像真实结构。

  只有测试调用的方法应放在测试工具中,而不是生产代码中。

  准备对 mock 本身作断言?
    取消 mock 或删除该断言。

测试与实现一同交付

TDD 循环——失败测试、最小实现、重构——就是“完成”的含义。交付行为所需且仅限 这些测试:琐碎代码和面向人类的说明文字不值得测试;只为满足流程而编写的测试会 永远产生维护成本。

变异检查

完成前,在脑中改变生产代码;对于每一种现实可能发生的变异,都应至少有一个测试失败:

  • 错误常量或参数
  • 错误分支处理器
  • 缺失的状态变更或副作用
  • 空返回值或默认返回值
  • 缺少对零值、空值、nil、未授权或格式错误输入的验证

若某项变异没有任何测试能捕获,说明该行为没有保护——或者测试只是同义反复。

快速参考

当你…… 应该做什么
编写任何测试 指出它能捕获的破坏——必须是缺陷,而不是决策
构造期望值 手工推导;绝不使用被测代码
测试脚本或文档 运行它/对使用者进行压力测试;绝不要 grep 文本
想测试某个依赖项 测试你的边界契约,而不是它已有文档的机制
想对被 mock 的元素作断言 测试真实组件,或取消 mock
准备 mock 一个方法 了解其副作用;mock 缓慢/外部的层级
构造 mock 响应 完整镜像真实结构
需要仅供测试使用的清理逻辑 放入测试工具
mock 设置开始膨胀 切换到使用真实组件的集成测试
完成一个测试文件 运行变异检查

警告信号

  • 设置和断言共享同一对象,保证它们必然相等
  • 测试只能因为 panic、崩溃或选择器缺失而失败
  • 每次有意变更都会让测试失败,意外破坏却从不触发它
  • 期望值隐藏在循环、构建器或辅助函数之后
  • 测试 grep 源文本,或断言已删除符号必须继续保持删除状态
  • 如果只剩框架,这个测试仍然有意义
  • 测试只为覆盖率而存在,没有检查任何副作用或结果
  • 断言检查 *-mock 测试 ID,或删除 mock 后就失败
  • 某个方法只从测试文件中调用
  • mock 设置超过测试的一半,或你无法解释为何需要它
  • “为了保险”而 mock