用途
Testing Anti-Patterns 是一份面向开发者的测试实践指南,用于识别和纠正测试代码中的常见错误做法。它的核心主张是:测试必须验证真实行为,而不是 mock 行为;mock 是隔离手段,不是被测对象。文档强调,严格遵循 TDD(测试驱动开发)可以预防这些反模式。
三条铁律
- 绝不测试 mock 行为
- 绝不向生产类添加仅供测试使用的方法
- 绝不在未理解依赖关系的情况下进行 mock
主要反模式
反模式 1:测试 mock 行为
错误做法是断言 mock 元素存在,例如用 screen.getByTestId('sidebar-mock') 判断侧边栏是否渲染。这样验证的是 mock 是否工作,而不是组件是否工作;测试在 mock 存在时通过、不存在时失败,无法反映真实行为。修正方式是测试真实组件,或干脆不 mock 该组件,改用 screen.getByRole('navigation') 这类基于真实语义的断言。
门禁自检:在对任何 mock 元素断言之前,先问“我是在测试真实组件行为,还是只测试 mock 是否存在?”如果答案是后者,就删除断言或取消 mock。
反模式 2:生产代码中的测试专用方法
错误做法是在生产类中加入只在测试中调用的方法,例如给 Session 类添加 destroy(),仅在 afterEach 中使用。这会污染生产类、在生产环境被误调用时存在风险、违反 YAGNI 与关注点分离,并混淆对象生命周期与实体生命周期。修正方式是把清理逻辑放到测试工具函数中,例如在 test-utils/ 中提供 cleanupSession(session)。
门禁自检:在向生产类添加方法前,先问“这是否只被测试使用?”如果是,就不要添加,改放到测试工具中;再问“这个类是否拥有该资源的生命周期?”如果不是,说明方法放错了类。
反模式 3:未理解依赖就 mock
错误做法是 mock 掉测试所依赖的副作用。例如测试“检测重复服务器”时 mock 了 ToolCatalog 的 discoverAndCacheTools,导致配置写入被阻止,第二次 addServer 本应抛错却没有抛错。原因是被 mock 的方法带有测试所依赖的副作用,过度 mock 反而破坏了真实行为。修正方式是在正确的层级 mock,例如只 mock 慢速的 MCPServerManager,保留测试所需的配置写入行为。
门禁自检:在 mock 任何方法前先停下来,依次问:真实方法有哪些副作用?本测试是否依赖其中某些副作用?我是否完全理解这个测试需要什么?如果依赖副作用,就在更低层级 mock(真正的慢速或外部操作),或使用能保留必要行为的测试替身,而不是 mock 测试所依赖的高层方法。如果不确定测试依赖什么,先用真实实现运行测试,观察实际需要发生什么,再在正确层级加入最小 mock。
红旗信号包括:“为了保险起见先 mock 一下”“这可能很慢,最好 mock 掉”“不理解依赖链就 mock”。
反模式 4:不完整的 mock
错误做法是只 mock 自己认为需要的字段,例如 mock 响应只包含 status 和 data,缺少下游代码会用到的 metadata。这会导致:部分 mock 隐藏了结构假设;下游代码可能依赖未包含的字段,造成静默失败;测试通过但集成失败;产生虚假信心。铁律是:mock 必须完整镜像现实中存在的数据结构,而不只是当前测试用到的字段。修正方式是补齐真实 API 返回的全部字段,例如加入 metadata: { requestId: 'req-789', timestamp: 1234567890 }。
门禁自检:创建 mock 响应前,先确认真实 API 响应包含哪些字段;查阅文档或示例中的实际响应;包含系统下游可能消费的所有字段;验证 mock 与真实响应结构完全一致。如果不确定,就包含所有有文档记录的字段。
反模式 5:把集成测试当作事后补充
错误做法是“实现完成、没有测试、声称可以测试了”。测试是实现的一部分,不是可选的后续步骤;TDD 本应在此前发现问题;没有测试就不能声称完成。修正方式是遵循 TDD 循环:先写失败测试、实现使其通过、重构,然后才声称完成。
mock 过于复杂的预警信号
- mock 的设置代码比测试逻辑还长
- 为了让测试通过而 mock 一切
- mock 缺少真实组件拥有的方法
- mock 一改测试就坏
此时应考虑:使用真实组件的集成测试往往比复杂 mock 更简单。
TDD 如何预防这些反模式
- 先写测试:迫使你思考究竟在测试什么
- 看它失败:确认测试测的是真实行为而非 mock
- 最小实现:避免测试专用方法混入
- 真实依赖:在 mock 之前先看清测试真正需要什么
如果测试的是 mock 行为,说明已经违反了 TDD——你在没有先让测试对真实代码失败的情况下就加入了 mock。
快速对照表
| 反模式 | 修正 |
|---|---|
| 断言 mock 元素 | 测试真实组件或取消 mock |
| 生产代码中的测试专用方法 | 移到测试工具中 |
| 未理解就 mock | 先理解依赖,最小化 mock |
| 不完整的 mock | 完整镜像真实 API |
| 测试作为事后补充 | TDD,测试先行 |
| 过于复杂的 mock | 考虑集成测试 |
红旗清单
- 断言检查
*-mock测试 ID - 只在测试文件中被调用的方法
- mock 设置占测试的 50% 以上
- 移除 mock 后测试就失败
- 无法解释为什么需要 mock
- “为了保险起见”而 mock
结论
mock 是隔离工具,不是被测对象。如果 TDD 揭示出你在测试 mock 行为,说明方向已经错了。修正方式是测试真实行为,或者重新质疑为什么要 mock。
限制
本文档为测试实践指南,未提供具体安装命令、平台支持范围、收费或价格信息,这些内容暂未提供。