能不改源码就切换 mock,前提是所有外部依赖通过参数或构造函数注入且类型为接口;需将 fetch 等调用抽象为可替换函数参数,用工厂函数统一创建服务实例,并在入口处控制实现注入,mock 实现须完整覆盖接口所有方法及错误路径。

能不改源码就切换 Mock,前提是业务逻辑里没有硬编码的 fetch、new ApiClient()、db.Query() 这类调用——所有外部依赖必须通过参数或构造函数进来,且依赖类型是接口(或抽象契约),不是具体实现。
把真实请求抽成可传入的函数参数
最轻量、见效最快的改造点:把原本写死在 Hook、组件或 Service 方法里的网络调用,改成由外层注入。不是“封装一个工具函数”,而是让业务逻辑彻底不持有任何具体实现。
常见错误现象:useEffect(() => { fetch('/api/user') }) —— fetch 是全局绑定,测试时无法拦截;axios.get('/api/user') 写在业务函数内部,Mock 只能打桩全局 axios,污染其他测试。
正确做法是让调用行为本身可替换:
- React Hook 示例:
const useUser = (requestFn = fetch) => { useEffect(() => { requestFn('/api/user') }, [requestFn]) } - 生产环境用默认值:
useUser()→ 走真实fetch - 测试/Mock 环境显式传入:
useUser(mockFetch)或useUser(axios.get) - 如果已有自定义 client,同样适用:
useUser(apiClient.getUser),只要签名匹配(接受 URL/参数,返回 Promise)
用工厂函数统一创建并注入服务实例
避免 new ApiClient() 散落在十几个文件里——那等于把实现细节焊死在业务线中,后续加日志、切环境、换 base URL 都得全局搜索替换。
应该集中导出一个工厂函数,并在应用入口一次性组装好所有服务:
- 定义工厂:
export const createApiClient = (options: { baseUrl: string }) => new ApiClient({ baseUrl: options.baseUrl }) - 入口组装:
const services = { api: createApiClient({ baseUrl: import.meta.env.VITE_API_URL }), logger: new ConsoleLogger() } - 后续所有模块都从
services取实例,而不是自己new - Mock 场景下,只需改工厂返回值:
createApiClient = () => new MockApiClient(),其余代码完全不动
Go / TypeScript / C++ 中必须用接口抽象隔离实现
没有接口,Mock 就无从谈起。因为 Mock 的本质是“提供另一个满足相同契约的实现”,而不是重写私有方法或打桩系统调用。
不同语言的关键差异:
- Go:
type UserRepository interface { GetUser(id int) (*User, error) }—— Mock 结构体只要实现这个方法即可,无需继承 - TypeScript:
interface UserProvider { get(id: number): Promise<user> }</user>—— 实际类必须implements UserProvider,否则 DI 容器无法校验类型 - C++:
class UserRepository { public: virtual ~UserRepository() = default; virtual User Get(int id) = 0; }—— 必须是纯虚类,且析构函数为 virtual,否则多态失效 - 错误示范:
func NewUserService(db *PostgreSQLDB)—— 注入的是具体类,无法 Mock;应改为func NewUserService(repo UserRepository)
构建时或运行时控制注入源头,而非改业务代码
最终决定用哪个实现的逻辑,必须收口到启动入口、构建脚本或配置层,不能散落在业务模块里。
典型控制方式:
- Go 主程序:
if flag.Lookup("mock").Value.String() == "true" { svc = NewUserService(&MockRepo{}) } else { svc = NewUserService(&DBRepo{}) } - TypeScript:
const provider = import.meta.env.MODE === 'test' ? mockServices : realServices,再统一provide('services', provider) - CMake:
option(USE_MOCK "Enable mock providers" OFF),控制是否编译mock_user_repo.cpp - 关键点:业务代码里永远只写
repo.GetUser(123),不出现任何if test {...} else {...}分支
最容易被忽略的是:Mock 实现必须完整覆盖接口所有方法,包括错误路径和边界行为——比如 GetUser(999) 应返回 error("not found"),而不仅是返回固定数据。否则测试看似通过,上线后遇到空数据或异常状态就会崩。










