5月27日 19:50
Jest Mock 怎么用?从 Mock 函数到模块替换全解析
为什么需要 Mock?
在单元测试中,被测代码往往依赖外部模块(如 API 请求、数据库、第三方库)。直接调用这些依赖会导致测试变慢、不稳定、难以控制返回值。Jest 的 Mock 功能可以替换依赖的行为,让测试专注于被测逻辑本身。
一、创建 Mock 函数
jest.fn() 是创建 Mock 函数最基本的方式,它会生成一个空函数并记录所有调用信息:
javascriptconst mockFn = jest.fn(); mockFn('hello'); mockFn('world'); console.log(mockFn.mock.calls); // [['hello'], ['world']] console.log(mockFn.mock.results); // [{ type: 'return', value: undefined }, { type: 'return', value: undefined }]
mockFn.mock 对象包含三个关键属性:
| 属性 | 说明 |
|---|---|
mock.calls | 每次调用的参数列表 |
mock.results | 每次调用的返回值 |
mock.instances | 每次调用时的 this 值 |
二、控制 Mock 返回值
mockReturnValue — 固定返回值
javascriptconst getAge = jest.fn().mockReturnValue(25); console.log(getAge()); // 25 console.log(getAge()); // 25(每次都返回相同值)
mockReturnValueOnce — 一次性返回值
javascriptconst getRandom = jest.fn() .mockReturnValueOnce(1) .mockReturnValueOnce(2) .mockReturnValue(0); console.log(getRandom()); // 1 console.log(getRandom()); // 2 console.log(getRandom()); // 0(Once 用完后回落到 mockReturnValue)
mockResolvedValue — 异步返回值
javascriptconst fetchUser = jest.fn().mockResolvedValue({ name: 'Alice' }); // 在测试中使用 async/await const user = await fetchUser(1); expect(user).toEqual({ name: 'Alice' });
mockResolvedValueOnce 同理,仅生效一次。
三、自定义 Mock 实现
当需要根据参数动态返回不同值时,使用 mockImplementation:
javascriptconst calculate = jest.fn().mockImplementation((a, b) => a + b); expect(calculate(1, 2)).toBe(3);
也可以在 jest.fn() 中直接传入实现:
javascriptconst greet = jest.fn(name => `Hello, ${name}!`);
进阶用法 — 根据调用次数返回不同值:
javascriptconst fn = jest.fn() .mockImplementationOnce(() => 'first') .mockImplementationOnce(() => 'second') .mockImplementation(() => 'default');
四、Mock 整个模块
这是实际项目中最常用的场景 — 替换外部模块的导出:
替换默认导出
javascript// api.js export default function fetchData() { return fetch('/api/data'); } // __tests__/component.test.js jest.mock('../api', () => ({ __esModule: true, default: jest.fn(() => Promise.resolve({ data: 'mocked' })) })); import fetchData from '../api'; test('使用模拟的 API 数据', async () => { const result = await fetchData(); expect(result).toEqual({ data: 'mocked' }); });
替换命名导出
javascript// utils.js export function formatDate(date) { /* ... */ } export function parseJSON(str) { /* ... */ } // 仅 Mock formatDate,保留 parseJSON 原始实现(Partial Mock) jest.mock('../utils', () => ({ ...jest.requireActual('../utils'), formatDate: jest.fn(() => '2026-01-01') }));
使用 __mocks__ 目录自动 Mock
在模块同目录下创建 __mocks__/api.js:
javascript// __mocks__/api.js export default function fetchData() { return Promise.resolve({ data: 'from automock' }); }
测试文件只需声明 jest.mock('../api'),Jest 会自动查找 __mocks__ 目录。
五、SpyOn — 监视真实函数
jest.spyOn 在不替换原函数的情况下追踪调用,也可以按需 Mock:
javascriptconst math = { add: (a, b) => a + b, }; test('spy 追踪调用但不改变行为', () => { const spy = jest.spyOn(math, 'add'); expect(math.add(1, 2)).toBe(3); // 原函数正常执行 expect(spy).toHaveBeenCalledWith(1, 2); // 同时记录了调用 }); test('spy 也可以临时替换实现', () => { jest.spyOn(math, 'add').mockReturnValue(999); expect(math.add(1, 2)).toBe(999); // 被替换了 math.add.mockRestore(); // 恢复原函数 });
六、常用断言
| 断言 | 说明 |
|---|---|
toHaveBeenCalled() | 至少被调用一次 |
toHaveBeenCalledTimes(n) | 被调用了 n 次 |
toHaveBeenCalledWith(...args) | 曾用指定参数调用 |
toHaveBeenLastCalledWith(...args) | 最后一次调用的参数 |
toHaveReturnedWith(value) | 曾返回指定值 |
toHaveLastReturnedWith(value) | 最后一次返回的值 |
toHaveReturnedTimes(n) | 成功返回了 n 次 |
七、清理 Mock
测试之间未清理的 Mock 会导致状态泄漏,务必在 afterEach 或 afterAll 中清理:
javascriptafterEach(() => { jest.clearAllMocks(); // 清除所有 mock.calls、mock.results,但保留实现 }); afterAll(() => { jest.restoreAllMocks(); // 恢复所有 spyOn 的原始实现 });
| 方法 | 效果 |
|---|---|
jest.clearAllMocks() | 清除调用记录,保留 mock 实现 |
jest.resetAllMocks() | 清除调用记录 + 清除 mock 实现(恢复为空函数) |
jest.restoreAllMocks() | 恢复 spyOn 的原始实现 |
八、常见问题与最佳实践
问题1:Mock 不生效
jest.mock 会被提升(hoisted)到文件顶部,如果回调中使用了变量,该变量可能尚未定义。解决方案:
javascript// 错误 — mockFactory 尚未定义 const mockFactory = () => jest.fn(); jest.mock('../module', mockFactory); // 正确 — 使用动态函数 jest.mock('../module', () => ({ myMethod: jest.fn() }));
问题2:Timer Mock
测试 setTimeout、setInterval 相关逻辑时:
javascriptjest.useFakeTimers(); test('延迟执行', () => { const callback = jest.fn(); setTimeout(callback, 1000); jest.advanceTimersByTime(1000); expect(callback).toHaveBeenCalled(); });
最佳实践
- Mock 外部依赖,不 Mock 被测代码本身 — 否则测试失去意义
- 优先使用 spyOn 而非 jest.fn 替换 — 便于恢复原始行为
- 每个测试前确保 Mock 状态干净 — 避免测试间相互影响
- Mock 的行为应尽量贴近真实 — 否则测试通过但代码可能在生产环境失败
- 不要过度 Mock — 如果一个测试中 Mock 了超过 3 个依赖,考虑是否测试粒度不对