只要接口定义稳定、依赖创建逻辑外移,就能在不改业务代码前提下切换 Mock;关键在于注入点是否暴露可控,需通过构造函数注入、接口/纯虚类抽象、Mock严格遵循契约、统一前置切换入口,并同步更新Mock以匹配接口演化。

只要接口定义稳定、依赖创建逻辑外移,就能在不改任何业务代码的前提下切换 Mock —— 关键不是“能不能”,而是“注入点是否暴露、是否可控”。
把 new 从类内部抽出来,改成构造函数参数
硬编码 new DatabaseRepo() 或 new ApiClient() 是一切问题的起点。它让实例在编译期就绑定,运行时无法替换。
- 错误做法:
class OrderService { private repo = new DatabaseRepo(); }—— 测试时根本拿不到 repo 的控制权 - 正确做法:
class OrderService { constructor(private repo: UserProvider) {} }—— 测试时直接传new MockUserProvider() - Go 中必须用接口类型(如
UserRepository)而非具体结构体做参数,否则 mock 无法实现多态 - C++ 需用纯虚类(
class IUserProvider { public: virtual User Get(int) = 0; };),避免 final 类或私有构造导致 mock 失败
Mock 实现必须严格遵循接口契约,不能只“能跑”
一个只返回固定数据但不校验参数、不模拟错误路径的 Mock,会在集成阶段暴雷。它不是占位符,是契约的镜像。
-
MockUserProvider.Get(123)必须返回符合User类型的结构,字段名、嵌套层级、空值处理都要一致 - 要覆盖边界:比如
Get(0)或Get(-1)应返回error("invalid id"),而非 panic 或静默忽略 - 记录调用痕迹:
mock.Calls.Count("Get")和mock.Calls.Args("Get", 0)是验证逻辑是否被触发的关键依据 - TypeScript 中若用
jest.fn()模拟函数,需用mockImplementationOnce控制不同调用返回不同结果,否则难测重试逻辑
切换入口必须在构建或启动时,而非运行中动态判断
靠 if (process.env.NODE_ENV === 'test') 在业务代码里分支加载模块,等于把环境逻辑又耦合回去了。切换点必须统一、前置、不可绕过。
- Go 主程序中:用
-mockflag 控制初始化分支,if mockFlag { svc = NewUserService(&MockRepo{}) } else { svc = NewUserService(NewDBRepo()) } - CMake 中:用
option(USE_MOCK "Enable mock providers" OFF),仅当开启时才add_subdirectory(test/mocks) - TypeScript:在
main.ts或 DI 容器注册表里,根据import.meta.env.MODE决定bind(UserProvider).to(MockUserProvider)还是.to(HttpUserProvider) - 切忌在组件或 Hook 内部写
const api = import.meta.env.DEV ? mockApi : realApi—— 这会让 mock 逻辑污染每一处调用点
最容易被忽略的是接口演化:一旦 UserProvider.Get() 增加了新参数或返回字段,所有 Mock 实现都必须同步更新,否则测试通过但线上崩溃。这不是技术问题,是协作契约问题 —— 接口即协议,mock 即履约证明。

















