5月27日 19:50

Jest Mock 怎么用?从 Mock 函数到模块替换全解析

为什么需要 Mock?

在单元测试中,被测代码往往依赖外部模块(如 API 请求、数据库、第三方库)。直接调用这些依赖会导致测试变慢、不稳定、难以控制返回值。Jest 的 Mock 功能可以替换依赖的行为,让测试专注于被测逻辑本身。


一、创建 Mock 函数

jest.fn() 是创建 Mock 函数最基本的方式,它会生成一个空函数并记录所有调用信息:

javascript
const 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 — 固定返回值

javascript
const getAge = jest.fn().mockReturnValue(25); console.log(getAge()); // 25 console.log(getAge()); // 25(每次都返回相同值)

mockReturnValueOnce — 一次性返回值

javascript
const getRandom = jest.fn() .mockReturnValueOnce(1) .mockReturnValueOnce(2) .mockReturnValue(0); console.log(getRandom()); // 1 console.log(getRandom()); // 2 console.log(getRandom()); // 0(Once 用完后回落到 mockReturnValue)

mockResolvedValue — 异步返回值

javascript
const fetchUser = jest.fn().mockResolvedValue({ name: 'Alice' }); // 在测试中使用 async/await const user = await fetchUser(1); expect(user).toEqual({ name: 'Alice' });

mockResolvedValueOnce 同理,仅生效一次。


三、自定义 Mock 实现

当需要根据参数动态返回不同值时,使用 mockImplementation

javascript
const calculate = jest.fn().mockImplementation((a, b) => a + b); expect(calculate(1, 2)).toBe(3);

也可以在 jest.fn() 中直接传入实现:

javascript
const greet = jest.fn(name => `Hello, ${name}!`);

进阶用法 — 根据调用次数返回不同值:

javascript
const 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:

javascript
const 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 会导致状态泄漏,务必在 afterEachafterAll 中清理:

javascript
afterEach(() => { 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

测试 setTimeoutsetInterval 相关逻辑时:

javascript
jest.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 个依赖,考虑是否测试粒度不对
标签:Jest