Files
EapilSkillMarket/plugins/codex/plugins/superpowers-zh/skills/test-driven-development/testing-anti-patterns.md
T
KeyInfo Bot 59c6b9f6cb Add Superpowers Chinese marketplace plugin
Constraint: Keep the official Superpowers plugin available under its existing ID while publishing the localized fork with a distinct ASCII ID.

Rejected: Reusing the upstream superpowers manifest name | It collides with the official marketplace entry.

Confidence: high

Scope-risk: narrow

Directive: Continue syncing this plugin from AreChen/superpowers-zh and preserve hooks: {}.

Tested: Marketplace validator, Codex local install/list/remove, KeyInfo adapter tests, upstream Codex manifest and package checks except its platform-specific tar epoch assertion.
2026-07-14 10:30:23 +08:00

8.7 KiB
Raw Blame History

测试反模式

在以下情况下加载此参考: 编写或修改测试、添加 mock,或想要向生产代码中添加仅供测试使用的方法时。

概述

测试必须验证真实行为,而不是 mock 行为。mock 是用于隔离的手段,而不是被测试的对象。

核心原则: 测试代码做了什么,而不是 mock 做了什么。

严格遵循 TDD 可防止这些反模式。

铁律

1. 绝不测试 mock 行为
2. 绝不向生产类添加仅供测试使用的方法
3. 绝不在不了解依赖项的情况下进行 mock

反模式 1:测试 Mock 行为

违规做法:

// ❌ 错误:测试 mock 是否存在
test('渲染侧边栏', () => {
  render(<Page />);
  expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
});

为什么这是错误的:

  • 你验证的是 mock 是否有效,而不是组件是否有效
  • mock 存在时测试通过,不存在时测试失败
  • 这无法告诉你任何有关真实行为的信息

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

修复方法:

// ✅ 正确:测试真实组件,或者不要对其进行 mock
test('渲染侧边栏', () => {
  render(<Page />);  // 不要 mock 侧边栏
  expect(screen.getByRole('navigation')).toBeInTheDocument();
});

// 或者,如果必须对侧边栏进行 mock 以实现隔离:
// 不要对 mock 进行断言——测试侧边栏存在时 Page 的行为

门函数

在对任何 mock 元素进行断言之前:
  问:“我是在测试真实的组件行为,还是仅仅测试 mock 是否存在?”

  如果测试的是 mock 是否存在:
    停止——删除该断言,或取消对该组件的 mock

  改为测试真实行为

反模式 2:生产代码中的仅供测试使用的方法

违规:

// ❌ 错误:destroy() 仅在测试中使用
class Session {
  async destroy() {  // 看起来像生产 API
    await this._workspaceManager?.destroyWorkspace(this.id);
    // ... 清理
  }
}

// 在测试中
afterEach(() => session.destroy());

为什么这是错误的:

  • 生产类被仅供测试使用的代码污染
  • 如果在生产环境中被意外调用,会很危险
  • 违反 YAGNI 原则和关注点分离原则
  • 混淆了对象生命周期与实体生命周期

修复方法:

// ✅ 正确:由测试工具处理测试清理
// Session 没有 destroy()——它在生产环境中是无状态的

// 在 test-utils/ 中
export async function cleanupSession(session: Session) {
  const workspace = session.getWorkspaceInfo();
  if (workspace) {
    await workspaceManager.destroyWorkspace(workspace.id);
  }
}

// 在测试中
afterEach(() => cleanupSession(session));

门函数

在向生产类添加任何方法之前:
  询问:“这是否仅供测试使用?”

  如果是:
    停止——不要添加它
    改为将它放入测试工具中

  询问:“这个类是否拥有此资源的生命周期?”

  如果不是:
    停止——这个方法不属于这个类

反模式 3:在不了解的情况下进行模拟

违规做法:

// ❌ 不佳:模拟破坏了测试逻辑
test('检测重复服务器', () => {
  // 模拟阻止了测试所依赖的配置写入!
  vi.mock('ToolCatalog', () => ({
    discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
  }));

  await addServer(config);
  await addServer(config);  // 应该抛出异常——但不会!
});

为什么这是错误的:

  • 被模拟的方法具有测试所依赖的副作用(写入配置)
  • 为了“保险”而过度模拟会破坏实际行为
  • 测试会因错误的原因通过,或以令人费解的方式失败

修复方法:

// ✅ 良好:在正确层级进行模拟
test('检测重复服务器', () => {
  // 模拟耗时的部分,保留测试所需的行为
  vi.mock('MCPServerManager'); // 只模拟耗时的服务器启动过程

  await addServer(config);  // 配置已写入
  await addServer(config);  // 检测到重复项 ✓
});

门函数

在模拟任何方法之前:
  停下——暂时不要模拟

  1. 问:“真实方法有哪些副作用?”
  2. 问:“此测试是否依赖其中的任何副作用?”
  3. 问:“我是否完全理解此测试需要什么?”

  如果依赖副作用:
    在更低层级进行模拟(实际耗时的操作/外部操作)
    或使用能够保留必要行为的测试替身
    不要模拟测试所依赖的高层方法

  如果不确定测试依赖什么:
    务必先使用真实实现运行测试
    观察实际必须发生什么
    然后才在正确层级添加最少量的模拟

  危险信号:
    - “为了保险,我把这个模拟掉”
    - “这可能会很慢,最好模拟掉”
    - 尚未理解依赖链就进行模拟

反模式 4:不完整的模拟

违规行为:

// ❌ 错误:部分模拟——只包含你认为需要的字段
const mockResponse = {
  status: 'success',
  data: { userId: '123', name: '爱丽丝' }
  // 缺少:下游代码使用的 metadata
};

// 稍后:当代码访问 response.metadata.requestId 时发生错误

为什么这是错误的:

  • 部分模拟会掩盖结构性假设——你只模拟了自己知道的字段
  • 下游代码可能依赖你未包含的字段——静默失败
  • 测试通过,但集成失败——模拟不完整,真实 API 是完整的
  • 虚假的信心——测试完全无法证明真实行为

**铁律:**模拟现实中实际存在的完整数据结构,而不只是当前测试使用的字段。

修复方法:

// ✅ 正确:与真实 API 的完整性保持一致
const mockResponse = {
  status: 'success',
  data: { userId: '123', name: '爱丽丝' },
  metadata: { requestId: 'req-789', timestamp: 1234567890 }
  // 真实 API 返回的所有字段
};

门函数

创建模拟响应之前:
  检查:“真实 API 响应包含哪些字段?”

  操作:
    1. 检查文档/示例中的实际 API 响应
    2. 包含系统可能在下游使用的所有字段
    3. 验证模拟是否与真实响应模式完全匹配

  关键要求:
    如果你正在创建模拟,就必须理解整个结构
    当代码依赖被省略的字段时,部分模拟会静默失败

  如果不确定:包含所有已记录在文档中的字段

反模式 5:把集成测试当作事后补充

违规做法:

✅ 实现完成
❌ 未编写测试
“已准备好进行测试”

为什么这是错误的:

  • 测试是实现的一部分,而不是可选的后续工作
  • TDD 本可以发现这一问题
  • 没有测试就不能声称已经完成

修正方法:

TDD 循环:
1. 编写一个失败的测试
2. 实现代码以使其通过
3. 重构
4. 然后才能声称完成

当模拟对象变得过于复杂时

警告信号:

  • 模拟对象的设置比测试逻辑还长
  • 为了让测试通过而模拟一切
  • 模拟对象缺少真实组件所拥有的方法
  • 模拟对象发生变化时测试就会失败

你的人类伙伴的问题:“我们需要在这里使用模拟对象吗?”

**考虑一下:**使用真实组件的集成测试通常比复杂的模拟对象更简单

TDD 可防止这些反模式

TDD 为什么有帮助:

  1. 先编写测试 → 迫使你思考自己实际在测试什么
  2. 观察它失败 → 确认测试检验的是真实行为,而不是模拟对象
  3. 最小化实现 → 不会悄然加入仅供测试使用的方法
  4. 真实依赖项 → 在进行模拟之前,你会先看到测试实际需要什么

如果你测试的是模拟对象的行为,就违反了 TDD——你在没有先观察测试针对真实代码失败的情况下就添加了模拟对象。

快速参考

反模式 修正方法
对模拟元素进行断言 测试真实组件,或取消对其模拟
生产代码中存在仅供测试使用的方法 将其移至测试工具中
在不了解的情况下进行模拟 先了解依赖项,并尽可能少地模拟
不完整的模拟对象 完整复刻真实 API
将测试视为事后补充 TDD——测试优先
过于复杂的模拟对象 考虑使用集成测试

危险信号

  • 断言检查 *-mock 测试 ID
  • 仅在测试文件中调用的方法
  • 模拟对象的设置占测试的 >50%
  • 移除模拟对象时测试失败
  • 无法解释为什么需要模拟对象
  • “只是为了保险起见”而进行模拟

核心结论

模拟对象是用于隔离的工具,而不是测试对象。

如果 TDD 揭示你测试的是模拟对象的行为,那就说明你做错了。