在 Jest 测试框架中,jest.fn() 是一个强大且常用的工具,用于创建模拟函数,它不仅能替代真实的函数实现,还能追踪函数的调用情况、参数和返回值,从而让我们能够独立地测试代码逻辑,不正确的使用方式常常会导致各种令人困惑的报错,本文将深入剖析 jest.fn() 的常见报错场景,并提供清晰的调试策略与解决方案。

常见报错场景与解析
理解错误发生的原因是解决问题的第一步,以下是与 jest.fn() 相关的几个典型报错场景。
.toHaveBeenCalled() 匹配器失败
这是最常见的新手错误,测试断言某个模拟函数被调用了,但 Jest 报告它从未被调用。
报错信息可能类似:
Expected mock function to have been called, but it was not called.
原因分析:
- 模拟未正确注入:你创建了模拟函数,但没有将它传递给正在测试的代码,被测试的模块依然在调用原始的真实函数。
 - 代码逻辑问题:由于 
if条件不满足、循环未执行或异常提前返回,导致包含模拟函数调用的代码路径根本没有被执行。 
示例与解决:
// 错误示例:模拟函数未被注入
const mockCallback = jest.fn();
function fetchData(callback) {
  // ... 模拟数据获取
  callback('data'); // 这里调用的是内部的真实逻辑,而非我们的 mockCallback
}
fetchData(); // 忘记传入 mockCallback
expect(mockCallback).toHaveBeenCalled(); // 报错!
// 正确示例:将模拟函数作为参数传入
const mockCallback = jest.fn();
function fetchData(callback) {
  callback('data');
}
fetchData(mockCallback); // 正确注入
expect(mockCallback).toHaveBeenCalled(); // 通过
.toHaveBeenCalledWith(...) 参数不匹配
当你断言模拟函数被特定参数调用时,但实际传入的参数与预期不符。
报错信息可能类似:
Expected mock function to have been called with: ["expectedArg"], but it was called with: ["actualArg"].
原因分析:

- 参数值错误:测试代码传递给函数的参数值与你的预期不同。
 - 参数类型或引用错误:对于对象或数组,Jest 使用 
Object.is进行比较,因此内容相同但引用不同的对象会被视为不相等,除非你使用了自定义匹配器。 
解决策略:
仔细检查调用点的代码,确保传递的参数完全符合预期,对于复杂对象,可以使用 expect.objectContaining() 等匹配器进行部分匹配。
模拟函数返回 undefined
当一个被模拟的函数需要返回一个值以驱动后续逻辑时,若忘记配置其返回行为,它默认会返回 undefined,可能导致后续测试失败。
原因分析:
jest.fn() 创建的函数默认实现是空的,即 return undefined;,如果你的代码依赖于这个函数的返回值(if (result)),就会出错。
解决策略:
使用 .mockReturnValue(value) 或 .mockReturnValueOnce(value) 来为模拟函数设置返回值,如果需要更复杂的逻辑,可以使用 .mockImplementation(fn)。
const mockApi = jest.fn(); // 错误:忘记设置返回值 // mockApi(); // 返回 undefined // expect(mockApi()).toBe(true); // 失败 // 正确:设置返回值 mockApi.mockReturnValue(true); expect(mockApi()).toBe(true); // 通过
调试策略与最佳实践
面对报错时,除了阅读错误信息,还应主动进行调试。
- 
检查模拟状态:每个
jest.fn()创建的函数都有一个.mock属性,它是一个对象,记录了所有关于该函数的调用信息,这是最强大的调试工具。.mock.calls: 一个二维数组,记录了每次调用的参数。.mock.results: 一个数组,记录了每次调用的返回值。.mock.instances: 一个数组,记录了每次使用new调用时的this实例。
在测试失败时,可以这样打印信息:
test('debug mock', () => { const mockFn = jest.fn(); // ... 一些调用 mockFn 的复杂逻辑 console.log(mockFn.mock.calls); // 查看所有调用的参数 console.log(mockFn.mock.results); // 查看所有返回值 expect(mockFn).toHaveBeenCalledWith('expected'); }); - 
模块级别的模拟:如果需要模拟一个模块的一部分,或者一个被测模块内部直接
import的函数,使用jest.mock(path, factory)是更彻底和可靠的方法,它能确保模块内的所有引用都指向模拟函数。
 
错误速查表
| 错误类型 | 常见表现 | 解决思路 | 
|---|---|---|
| 未被调用 | ...have been called, but it was not called. | 
检查模拟函数是否正确注入到被测代码中;检查代码逻辑是否走到了调用点。 | 
| 参数不匹配 | ...called with: [arg1], but it was called with: [arg2]. | 
核对调用时传入的参数值、顺序和类型;使用 console.log 打印 .mock.calls。 | 
| 返回值问题 | 后续逻辑因 undefined 导致失败 | 
使用 .mockReturnValue() 或 .mockImplementation() 设置预期的返回值。 | 
| 异步错误 | 测试超时或 Promise 未处理 | 对于异步函数,使用 .mockResolvedValue 或 .mockRejectedValue,并确保测试中 await 了异步操作。 | 
相关问答 FAQs
Q1: jest.fn() 和 jest.spyOn() 有什么区别?我应该用哪个?
A: 两者都用于创建模拟函数,但核心区别在于作用对象和原始实现的保留。
jest.fn():从零开始创建一个全新的、独立的模拟函数,它没有任何原始实现。jest.spyOn(object, methodName):作用于一个已存在的对象方法,它会创建一个“间谍”函数,该函数会调用原始实现,同时记录调用信息,你可以选择用.mockImplementation()来覆盖原始实现,并且可以用.mockRestore()来完全恢复原始方法。
选择建议:
- 当你想完全替换一个函数,或者它是一个独立的回调函数时,使用 
jest.fn()。 - 当你想测试一个对象上的方法是否被调用,但又希望它在默认情况下保持原有功能时,使用 
jest.spyOn(),这在测试类实例方法时尤为有用。 
Q2: 如何模拟一个模块的默认导出函数?
A: 模拟默认导出需要使用 jest.mock() 并提供一个工厂函数,对于 ES6 模块,你需要确保模拟的对象具有 __esModule: true 标志,并提供一个 default 属性。
假设有一个 utils.js 文件:
// utils.js
export default function fetchData() {
  return 'real data';
}
在你的测试文件中可以这样模拟:
// test.test.js
import fetchData from './utils';
// 使用 jest.mock 和工厂函数来模拟默认导出
jest.mock('./utils', () => ({
  __esModule: true, // 此属性对于 ES6 模块模拟至关重要
  default: jest.fn(() => 'mocked data'), // 将默认导出模拟为一个函数
}));
// 现在你可以像测试普通 jest.fn 一样测试它
test('fetchData should return mocked data', () => {
  expect(fetchData()).toBe('mocked data');
  expect(fetchData).toHaveBeenCalled();
});