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.
8.7 KiB
8.7 KiB
测试反模式
在以下情况下加载此参考: 编写或修改测试、添加 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 为什么有帮助:
- 先编写测试 → 迫使你思考自己实际在测试什么
- 观察它失败 → 确认测试检验的是真实行为,而不是模拟对象
- 最小化实现 → 不会悄然加入仅供测试使用的方法
- 真实依赖项 → 在进行模拟之前,你会先看到测试实际需要什么
如果你测试的是模拟对象的行为,就违反了 TDD——你在没有先观察测试针对真实代码失败的情况下就添加了模拟对象。
快速参考
| 反模式 | 修正方法 |
|---|---|
| 对模拟元素进行断言 | 测试真实组件,或取消对其模拟 |
| 生产代码中存在仅供测试使用的方法 | 将其移至测试工具中 |
| 在不了解的情况下进行模拟 | 先了解依赖项,并尽可能少地模拟 |
| 不完整的模拟对象 | 完整复刻真实 API |
| 将测试视为事后补充 | TDD——测试优先 |
| 过于复杂的模拟对象 | 考虑使用集成测试 |
危险信号
- 断言检查
*-mock测试 ID - 仅在测试文件中调用的方法
- 模拟对象的设置占测试的 >50%
- 移除模拟对象时测试失败
- 无法解释为什么需要模拟对象
- “只是为了保险起见”而进行模拟
核心结论
模拟对象是用于隔离的工具,而不是测试对象。
如果 TDD 揭示你测试的是模拟对象的行为,那就说明你做错了。