mcpField
T
testing-anti-patterns

testing-anti-patterns

github.com/nickcrew测试与质量6285 次安装18 周前更新
代码仓库

关于 testing-anti-patterns

面向开发者的测试技能,帮助识别并避免常见的模拟测试问题。重点是验证真实行为、谨慎使用模拟、补全模拟数据,并通过 TDD 编写测试。

主要功能

  1. 01避免断言模拟对象的行为
  2. 02不在生产代码中加入测试专用方法
  3. 03模拟前先理解依赖
  4. 04按真实结构补全模拟数据
  5. 05采用 TDD 并考虑集成测试

适用场景

  1. 01检查断言模拟组件的测试
  2. 02将测试清理逻辑移出生产类
  3. 03排查模拟副作用导致的测试问题
  4. 04判断复杂模拟是否应改用集成测试

安装

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

$npx skills add https://github.com/nickcrew/claude-cortex --skill testing-anti-patterns

安装后在 Claude Code 中输入/testing-anti-patterns确认已加载。

详细介绍

测试反模式

概述

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

核心原则: 测试代码的行为,而不是 Mock 的行为。

严格遵循 TDD 可避免这些反模式。

铁律

1. NEVER test mock behavior
2. NEVER add test-only methods to production classes
3. NEVER mock without understanding dependencies

反模式 1:测试 Mock 行为

违规做法:

// ❌ BAD: Testing that the mock exists
test('renders sidebar', () => {
  render(<Page />);
  expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
});

这为什么不对:

  • 你验证的是 Mock 能否正常工作,而不是组件能否正常工作
  • Mock 存在时测试通过,不存在时测试失败
  • 无法让你了解任何真实行为

你的搭档提出的纠正问题:“我们是在测试 Mock 的行为吗?”

修正方法:

// ✅ GOOD: Test real component or don't mock it
test('renders sidebar', () => {
  render(<Page />);  // Don't mock sidebar
  expect(screen.getByRole('navigation')).toBeInTheDocument();
});

// OR if sidebar must be mocked for isolation:
// Don't assert on the mock - test Page's behavior with sidebar present
闸门函数
BEFORE asserting on any mock element:
  Ask: "Am I testing real component behavior or just mock existence?"

  IF testing mock existence:
    STOP - Delete the assertion or unmock the component

  Test real behavior instead

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

违规做法:

// ❌ BAD: destroy() only used in tests
class Session {
  async destroy() {  // Looks like production API!
    await this._workspaceManager?.destroyWorkspace(this.id);
    // ... cleanup
  }
}

// In tests
afterEach(() => session.destroy());

这为什么不对:

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

修正方法:

// ✅ GOOD: Test utilities handle test cleanup
// Session has no destroy() - it's stateless in production

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

// In tests
afterEach(() => cleanupSession(session));
闸门函数
BEFORE adding any method to production class:
  Ask: "Is this only used by tests?"

  IF yes:
    STOP - Don't add it
    Put it in test utilities instead

  Ask: "Does this class own this resource's lifecycle?"

  IF no:
    STOP - Wrong class for this method

反模式 3:未理解就使用 Mock

违规做法:

// ❌ BAD: Mock breaks test logic
test('detects duplicate server', () => {
  // Mock prevents config write that test depends on!
  vi.mock('ToolCatalog', () => ({
    discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
  }));

  await addServer(config);
  await addServer(config);  // Should throw - but won't!
});

这为什么不对:

  • 被 Mock 的方法有测试所依赖的副作用(写入配置)
  • 为了“保险”而过度使用 Mock,会破坏实际行为
  • 测试因错误原因通过,或莫名其妙地失败

修正方法:

// ✅ GOOD: Mock at correct level
test('detects duplicate server', () => {
  // Mock the slow part, preserve behavior test needs
  vi.mock('MCPServerManager'); // Just mock slow server startup

  await addServer(config);  // Config written
  await addServer(config);  // Duplicate detected ✓
});
闸门函数
BEFORE mocking any method:
  STOP - Don't mock yet

  1. Ask: "What side effects does the real method have?"
  2. Ask: "Does this test depend on any of those side effects?"
  3. Ask: "Do I fully understand what this test needs?"

  IF depends on side effects:
    Mock at lower level (the actual slow/external operation)
    OR use test doubles that preserve necessary behavior
    NOT the high-level method the test depends on

  IF unsure what test depends on:
    Run test with real implementation FIRST
    Observe what actually needs to happen
    THEN add minimal mocking at the right level

  Red flags:
    - "I'll mock this to be safe"
    - "This might be slow, better mock it"
    - Mocking without understanding the dependency chain

反模式 4:不完整的 Mock

违规做法:

// ❌ BAD: Partial mock - only fields you think you need
const mockResponse = {
  status: 'success',
  data: { userId: '123', name: 'Alice' }
  // Missing: metadata that downstream code uses
};

// Later: breaks when code accesses response.metadata.requestId

这为什么不对:

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

铁律: Mock 必须完整模拟现实中的数据结构,而不只是当前测试直接使用的字段。

修正方法:

// ✅ GOOD: Mirror real API completeness
const mockResponse = {
  status: 'success',
  data: { userId: '123', name: 'Alice' },
  metadata: { requestId: 'req-789', timestamp: 1234567890 }
  // All fields real API returns
};
闸门函数
BEFORE creating mock responses:
  Check: "What fields does the real API response contain?"

  Actions:
    1. Examine actual API response from docs/examples
    2. Include ALL fields system might consume downstream
    3. Verify mock matches real response schema completely

  Critical:
    If you're creating a mock, you must understand the ENTIRE structure
    Partial mocks fail silently when code depends on omitted fields

  If uncertain: Include all documented fields

反模式 5:事后才考虑集成测试

违规做法:

✅ Implementation complete
❌ No tests written
"Ready for testing"

这为什么不对:

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

修正方法:

TDD cycle:
1. Write failing test
2. Implement to pass
3. Refactor
4. THEN claim complete

Mock 变得过于复杂时

警示信号:

  • Mock 设置比测试逻辑还长
  • 为了让测试通过而 Mock 一切
  • Mock 缺少真实组件所具备的方法
  • Mock 变更时测试也会失败

你的搭档提出的问题:“这里真的需要使用 Mock 吗?”

请考虑: 使用真实组件的集成测试往往比复杂的 Mock 更简单

TDD 可避免这些反模式

TDD 的作用:

  1. 先写测试 → 迫使你思考自己实际在测试什么
  2. 观察测试失败 → 确认测试的是实际行为,而不是 Mock
  3. 采用最少实现 → 避免混入仅供测试使用的方法
  4. 使用真实依赖 → 在使用 Mock 之前,先弄清测试实际需要什么

如果你在测试 Mock 的行为,就违背了 TDD——你添加 Mock 前,没有先观察测试针对真实代码失败。

快速参考

反模式修正方法
对 Mock 元素进行断言测试真实组件,或移除 Mock
在生产代码中添加仅供测试使用的方法移到测试工具中
未理解就使用 Mock先理解依赖,再尽量少用 Mock
不完整的 Mock完整镜像真实 API
事后才考虑测试使用 TDD——先写测试
过于复杂的 Mock考虑集成测试

危险信号

  • 断言检查 *-mock 测试 ID
  • 某些方法只在测试文件中调用
  • Mock 设置占测试内容的 50% 以上
  • 移除 Mock 后测试失败
  • 无法解释为什么需要 Mock
  • 只是“为了保险”而使用 Mock

归根结底

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

如果 TDD 揭示你在测试 Mock 的行为,说明你走错了方向。

修正方法:测试真实行为,或质疑自己为何要使用 Mock。

评论

支持 Markdown