# 测试反模式

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

## 概述

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

**核心原则：** 测试代码做了什么，而不是 mock 做了什么。

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

## 铁律

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

## 反模式 1：测试 Mock 行为

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

**为什么这是错误的：**
- 你验证的是 mock 是否有效，而不是组件是否有效
- mock 存在时测试通过，不存在时测试失败
- 这无法告诉你任何有关真实行为的信息

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

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

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

### 门函数

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

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

  改为测试真实行为
```

## 反模式 2：生产代码中的仅供测试使用的方法
**违规：**
```typescript
// ❌ 错误：destroy() 仅在测试中使用
class Session {
  async destroy() {  // 看起来像生产 API！
    await this._workspaceManager?.destroyWorkspace(this.id);
    // ... 清理
  }
}

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

**为什么这是错误的：**
- 生产类被仅供测试使用的代码污染
- 如果在生产环境中被意外调用，会很危险
- 违反 YAGNI 原则和关注点分离原则
- 混淆了对象生命周期与实体生命周期

**修复方法：**
```typescript
// ✅ 正确：由测试工具处理测试清理
// 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：在不了解的情况下进行模拟
**违规做法：**
```typescript
// ❌ 不佳：模拟破坏了测试逻辑
test('检测重复服务器', () => {
  // 模拟阻止了测试所依赖的配置写入！
  vi.mock('ToolCatalog', () => ({
    discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
  }));

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

**为什么这是错误的：**
- 被模拟的方法具有测试所依赖的副作用（写入配置）
- 为了“保险”而过度模拟会破坏实际行为
- 测试会因错误的原因通过，或以令人费解的方式失败

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

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

### 门函数

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

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

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

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

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

## 反模式 4：不完整的模拟
**违规行为：**
```typescript
// ❌ 错误：部分模拟——只包含你认为需要的字段
const mockResponse = {
  status: 'success',
  data: { userId: '123', name: '爱丽丝' }
  // 缺少：下游代码使用的 metadata
};

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

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

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

**修复方法：**
```typescript
// ✅ 正确：与真实 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 揭示你测试的是模拟对象的行为，那就说明你做错了。
