测试反模式防范技能Skill testing-anti-patterns

这个技能用于识别和避免软件测试中的常见反模式,如测试模拟行为、在生成代码中添加测试专用方法等。它帮助开发者遵循测试驱动开发(TDD)原则,提高测试质量,确保代码可靠性。关键词:软件测试、反模式、模拟、TDD、代码质量、测试策略、模拟隔离、测试优化。

测试 0 次安装 0 次浏览 更新于 3/16/2026

name: testing-anti-patterns description: 在编写或修改测试、添加模拟或试图在生成代码中添加仅用于测试的方法时使用 - 防止测试模拟行为、用测试专用方法污染生成代码以及不理解依赖关系就进行模拟

测试反模式

概述

测试必须验证真实行为,而不是模拟行为。模拟是用来隔离的手段,而不是被测试的东西。

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

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

铁律

1. 永远不要测试模拟行为
2. 永远不要向生成类添加仅用于测试的方法
3. 永远不要在不理解依赖关系的情况下进行模拟

反模式1:测试模拟行为

违反:

// ❌ 错误:测试模拟的存在
test('renders sidebar', () => {
  render(<Page />);
  expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
});

为什么这是错误的:

  • 你在验证模拟是否工作,而不是组件是否工作
  • 测试在模拟存在时通过,不存在时失败
  • 没有告诉你关于真实行为的任何信息

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

修复:

// ✅ 正确:测试真实组件或不模拟它
test('renders sidebar', () => {
  render(<Page />);  // 不模拟侧边栏
  expect(screen.getByRole('navigation')).toBeInTheDocument();
});

// 或者如果必须模拟侧边栏以隔离:
// 不要断言模拟 - 测试侧边栏存在时页面的行为

关卡函数

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

  如果测试模拟存在:
    停止 - 删除断言或不模拟组件

  改为测试真实行为

反模式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('detects duplicate server', () => {
  // 模拟阻止了测试依赖的配置写入!
  vi.mock('ToolCatalog', () => ({
    discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
  }));

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

为什么这是错误的:

  • 模拟的方法有测试依赖的副作用(写入配置)
  • 过度模拟以“安全”破坏了实际行为
  • 测试因错误原因通过或神秘失败

修复:

// ✅ 正确:在正确的层级模拟
test('detects duplicate server', () => {
  // 模拟慢的部分,保留测试需要的行为
  vi.mock('MCPServerManager'); // 仅模拟慢的服务器启动

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

关卡函数

在模拟任何方法之前:
  停止 - 先不要模拟

  1. 问:“真实方法有哪些副作用?”
  2. 问:“这个测试依赖任何这些副作用吗?”
  3. 问:“我完全理解这个测试需要什么吗?”

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

  如果不确定测试依赖什么:
    先用真实实现运行测试
    观察实际需要发生什么
    然后在正确的层级添加最小模拟

  危险信号:
    - “我会模拟这个以安全”
    - “这个可能慢,最好模拟它”
    - 不理解依赖链就模拟

反模式4:不完整的模拟

违反:

// ❌ 错误:部分模拟 - 只包括你认为需要的字段
const mockResponse = {
  status: 'success',
  data: { userId: '123', name: 'Alice' }
  // 缺失:下游代码使用的元数据
};

// 稍后:当代码访问response.metadata.requestId时中断

为什么这是错误的:

  • 部分模拟隐藏结构假设 - 你只模拟了你知道的字段
  • 下游代码可能依赖你没有包括的字段 - 无声失败
  • 测试通过但集成失败 - 模拟不完整,真实API完整
  • 虚假信心 - 测试没有证明任何关于真实行为的东西

铁规则: 模拟现实中存在的完整数据结构,而不仅仅是你立即测试使用的字段。

修复:

// ✅ 正确:镜像真实API的完整性
const mockResponse = {
  status: 'success',
  data: { userId: '123', name: 'Alice' },
  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揭示你在测试模拟行为,你就走错了。

修复:测试真实行为或质疑为什么一开始要模拟。