构造函数注入比 new 更适合 Mock 切换,因它显式化依赖、避免编译期锁死实现、支持类型安全替换,且配合接口抽象与构建时切换可保障 Mock 可靠性与生命周期一致性。

能不改业务代码就切 Mock,前提是依赖创建必须移出类/函数内部,且所有实现都通过同一接口契约通信。
为什么构造函数注入比 new 更适合 Mock 切换
硬编码 new DatabaseRepo() 或 axios.create() 会把实现锁死在编译期;测试时无法替换,只能打桩全局函数(如 jest.mock('axios')),但这种方案脆弱、影响范围大、难以控制粒度。
- 构造注入让依赖关系显式化:
UserService的实例必须由外部提供UserProvider,测试时直接传new MockUserProvider()即可 - 避免 setter 注入:setter 允许对象处于未初始化状态,测试易漏设,且无法在编译/类型检查阶段暴露缺失依赖
- 接口抽象是前提:如果注入的是具体类(如
PostgreSQLClient),Mock 就得继承它——而很多数据库客户端是final或含私有字段,mock 成本极高
前端 Hook 中如何安全注入请求函数而非硬绑 fetch
写 useEffect(() => { fetch('/api/user') }) 等于放弃控制权;fetch 是全局绑定,无法按模块隔离 mock 行为。
- 把请求函数作为参数传入:
const useUser = (requestFn = fetch) => { ... },生产用默认fetch,测试传mockFetch - 若用
axios或自定义 client,同样适用:useUser(axios.get)或useUser(apiClient.getUser) - 注意:不要在 Hook 内部再封装一层“API 工具函数”,那只是把
new换成import,本质仍是硬依赖
Go/TS/PHP 中接口定义与 Mock 实现的关键约束
Mock 能否无缝替换,取决于接口是否真正覆盖了所有调用路径和错误分支。一个只定义 Get(id int) User 的接口,在真实场景中几乎必然失败。
- 必须包含 error 返回:
Get(id int) (User, error)(Go)、get(id: number): Promise<user></user>(TS)、public function get(int $id): User+ 异常声明(PHP) - Mock 实现要支持边界行为:比如
Get(0)返回nil, errors.New("invalid id"),Get(-1)模拟网络超时 - 记录调用痕迹用于断言:
mock.Calls.Count("Get") == 2,而不是只看返回值 - Mock 类应仅存在于
test/或__mocks__/目录,构建脚本需确保其不进入生产包
构建时切换 vs 运行时切换:哪个更可靠
运行时靠 NODE_ENV=test 或 -mock 参数动态选服务,看似灵活,实则容易因环境变量拼写错误、加载顺序错乱导致 Mock 失效且无提示。
- 构建时切换更可控:CMake 用
option(USE_MOCK OFF)控制源文件是否参与编译;Go 用//go:build mocktag 分离实现;TS 用tsconfig.json的paths映射 mock 版本 - 关键点在于:Mock 实现和真实实现必须共用同一接口文件,不能各自定义一套,否则类型系统无法校验兼容性
- 最易被忽略的是生命周期一致性:Mock 实例不能在每次
provide时新建(如provide('api', new ApiClient())),而应提前构造好,再统一provide('services', services)

















