Jest解构导入导致jest.requireActual失效、无法拦截原方法Mock的解决方法
Jest 单元测试 Mock 失效问题与解决方案
问题场景
需要为调用两个工具函数的controller编写单元测试,覆盖两个核心验证目标:
- 验证子工具函数是否被controller正确调用
- 验证controller返回的计算结果是否准确
项目涉及的代码如下:
- 工具函数实现
./utilities/sayMyName.jsexports.sayMyName = (name) => name; - 工具函数实现
./utilities/doubleNum.jsexports.doubleNum = (num) => num * 2; - 工具统一导出入口
./utilities/index.jsconst { doubleNum } = require("./doubleNum"); const { sayMyName } = require("./sayMyName"); module.exports = { doubleNum, sayMyName, }; - 待测试的controller实现
myController.jsconst { doubleNum, sayMyName } = require("./utilities"); exports.doubleMyNum = (num, name) => { const myName = sayMyName(name); return doubleNum(num); }; - 初版测试用例
myController.test.jsconst myController = require("./myController"); const utilities = require("./utilities"); jest.mock("./utilities"); describe("doubleNum", () => { test("should call sayMyName", () => { myController.doubleMyNum(2, "test name"); expect(utilities.sayMyName).toHaveBeenCalledWith("test name"); }); test("should double my number", () => { const { doubleNum } = jest.requireActual("./utilities"); expect(myController.doubleMyNum(2, "test name")).toBe(4); }); });
运行现象
- 第一个验证子函数调用的测试用例可以正常通过
- 第二个验证计算结果准确性的测试用例执行失败
问题根因
测试文件最开头通过jest.mock("./utilities")对整个工具模块做了全量Mock,按照Jest官方文档说明,jest.requireActual本应能获取模块的原始实现,但实际未生效。
核心原因:当模块被Mock后,myController.js已经在测试初始化阶段就加载完成,持有了Mock版本的工具函数引用。即使后续通过jest.requireActual获取了原始实现,已经加载完成的myController.js不会感知到模块导出的变更,仍会调用最初绑定的Mock函数。
叠加项目的写法特征,问题会更明显:
utilities/index.js做了统一的导出映射,将独立工具函数重新组合后对外暴露myController.js采用解构导入的方式从utilities模块引入方法,在模块加载时就将函数引用绑定到了本地作用域
可运行的解决方案
核心处理逻辑:在需要使用原始实现的测试用例中,先通过jest.resetModules清空模块缓存,再重新导入myController.js,让controller能拿到当前最新的模块导出内容。
调整后的可运行测试代码:
const myController = require("./myController"); const utilities = require("./utilities"); jest.mock("./utilities"); describe("doubleNum", () => { beforeEach(() => { jest.resetModules(); }); test("should call sayMyName", () => { const spy = jest.spyOn(utilities, "sayMyName"); myController.doubleMyNum(2, "test name"); expect(spy).toHaveBeenCalledWith("test name"); }); test("should double my number", () => { jest.mock("./utilities", () => { const { sayMyName, doubleNum } = jest.requireActual("./utilities"); return { sayMyName, doubleNum, }; }); // 重新导入myController,使utilities模块的原始实现生效 const myController = require("./myController"); expect(myController.doubleMyNum(2, "test name")).toBe(4); }); });
补充说明:如果
myController.js没有使用解构导入的写法,而是直接在调用时从utilities模块上取方法,上述问题的处理流程还可以进一步简化。
内容的提问来源于stack exchange,提问作者John Marsden
相关产品推荐
相关产品推荐

