在 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(); });
【版权声明】:本站所有内容均来自网络,若无意侵犯到您的权利,请及时与我们联系将尽快删除相关内容!
发表回复