如何在同一ES6模块中Mock内部函数?(Jest测试场景)
这个问题我之前也碰到过,其实是Jest模块mock的一个常见坑——你当前的写法不仅mock姿势不对,还没处理好同一模块内函数的调用关系,导致mock的getTransition根本没被触发。我给你拆解下问题,再给出几种可行的解决方案:
先说说你现有测试代码的问题
- mock的工厂函数写法错误:你写的
jest.mock("../src/helpers.js", () => { getTransition: myMock });里,大括号会被当成函数体,而不是要返回的对象,正确的写法应该给对象加个括号:() => ({ getTransition: myMock }),否则函数会默认返回undefined,等于没mock成功。 - 模块内部函数的调用关系没被mock覆盖:就算你修正了mock写法,原模块里的
getStyle调用的是模块内部定义的getTransition,而不是你mock后导出的那个——模块加载时内部函数的引用就已经绑定了,不会因为你mock了导出而改变内部的调用链路。
解决方案一:用jest.spyOnmock模块导出(推荐,无需改业务代码)
如果你的helpers.js是正常导出getStyle和getTransition的(不管是CommonJS还是ES模块),最省心的方式是用jest.spyOn来替换模块导出的getTransition,这样既能保留原模块的getStyle逻辑,又能监控它的调用情况:
测试代码调整如下:
// 导入整个模块 const helpers = require("../src/helpers.js"); it("gets style correctly and calls getTransition", () => { // 用spyOn监听模块导出的getTransition,替换成mock函数并指定返回值 const mockGetTransition = jest.spyOn(helpers, "getTransition") .mockReturnValue("mock-transition-value"); // 调用待测试的getStyle const style = helpers.getStyle({}, 100); // 校验返回结果 expect(style.height).toBe("100px"); expect(style.transition).toBe("mock-transition-value"); // 校验getTransition被调用了一次 expect(mockGetTransition).toHaveBeenCalledTimes(1); // 测试结束后恢复原函数,避免影响其他测试用例 mockGetTransition.mockRestore(); });
⚠️ 注意:这个方法生效的前提是,你的getStyle内部是通过模块的导出对象来调用getTransition的。如果原helpers.js里getStyle是直接调用内部的私有函数(比如function getTransition() {}然后直接getTransition()),那这个方法会失效,因为内部调用的不是导出的那个函数。
解决方案二:重构业务代码,让内部函数通过模块自身引用调用
如果你的getStyle确实是直接调用内部的getTransition,那需要稍微改一下业务代码,让它调用模块导出的版本,这样mock才能覆盖到:
修改helpers.js:
// CommonJS写法 function getTransition() { // 原逻辑 return "original-transition"; } function getStyle(obj, height) { // 把原来的直接调用getTransition(),改成调用模块导出的版本 const transition = module.exports.getTransition(); return { height: `${height}px`, transition }; } // 导出两个函数 module.exports = { getStyle, getTransition }; // 如果是ES模块,改成这样: // import * as self from './helpers.js'; // function getStyle(obj, height) { // const transition = self.getTransition(); // // ... // } // export { getStyle, getTransition };
改完业务代码后,你也可以用jest.mock结合jest.requireActual来mock,写法如下:
const myMock = jest.fn(() => "mock-transition"); // 保留原模块的其他导出,只mock getTransition jest.mock("../src/helpers.js", () => { const originalModule = jest.requireActual("../src/helpers.js"); return { ...originalModule, getTransition: myMock }; }); // 从mock后的模块导入getStyle const { getStyle } = require("../src/helpers.js"); it("gets style correctly", () => { const style = getStyle({}, 100); expect(style.height).toBe("100px"); expect(myMock).toHaveBeenCalledTimes(1); });
为什么你原来的mock.calls为空?
本质原因是:你mock的是模块导出的getTransition,但getStyle内部调用的是模块未导出的私有getTransition函数,两者不是同一个引用,所以你的mock函数根本没被执行,自然mock.calls是空的。只要让getStyle调用的是模块导出的版本,mock就能正常监控到调用了。
内容的提问来源于stack exchange,提问作者John K Doe

