jest.spyOn对导入函数失效:预期Mock函数被调用却未触发
我太懂这种头疼的感觉了——明明写了jest.spyOn去mock默认导出的函数,结果却一直报“Expected mock function to have been called, but it was not called”的错误。咱们先拆解下问题根源,再给你几个实用的解决办法。
问题到底出在哪?
你当前的代码里,testFn在模块加载阶段就直接绑定了foo模块默认导出的原始函数引用。而你在测试代码里用jest.spyOn(foo, 'default'),其实只是给当前测试模块里的foo.default加了个spy,但testFn内部用的还是它自己导入的那个原始foo函数——两者根本不是同一个引用,spy自然捕捉不到调用。
靠谱解决方案
这里有几种不同的思路,你可以根据自己的代码场景选:
方法1:用jest.mock直接mock整个foo模块
这是最直接有效的方式,因为jest.mock会在模块加载的最早期就替换掉foo模块的导出内容,这样testFn从一开始导入的就是mock后的函数:
// 测试文件 jest.mock('foo', () => ({ default: jest.fn() // 把默认导出替换成Jest mock函数 })); import testFn from 'test'; import foo from 'foo'; test('验证默认导出函数被调用', () => { testFn(); expect(foo.default).toHaveBeenCalledWith('text'); // 还能顺便验证传入的参数 });
方法2:调整模块结构,让依赖引用可被Spy
如果不想用jest.mock,可以修改test.js的结构,把foo的引用改成动态可替换的形式:
修改test.js:
// test.js import * as fooModule from 'foo'; // 导入整个模块对象 export default () => fooModule.default('text'); // 使用模块上的default属性
测试文件:
import testFn from 'test'; import * as fooModule from 'foo'; test('验证Spy捕捉到函数调用', () => { const spyFn = jest.spyOn(fooModule, 'default'); testFn(); expect(spyFn).toHaveBeenCalled(); spyFn.mockRestore(); // 测试结束后记得清理mock });
这样修改后,testFn使用的是fooModule.default这个动态引用,你在测试里用spyOn替换它时,testFn调用的就是被Spy后的函数了。
方法3:依赖注入(适合复杂场景)
如果testFn的逻辑允许,可以把foo作为可选参数传入,测试时直接传入mock函数就行:
修改test.js:
// test.js import foo from 'foo'; export default (fn = foo) => fn('text'); // 默认使用foo,允许传入自定义函数
测试文件:
import testFn from 'test'; test('验证函数被调用', () => { const mockFoo = jest.fn(); testFn(mockFoo); expect(mockFoo).toHaveBeenCalledWith('text'); });
这种方式最灵活,完全绕开了模块导入带来的引用绑定问题。
总结
核心问题就是ES模块的静态导入导致引用固定,jest.spyOn无法影响已经导入的函数引用。要么用jest.mock在模块加载阶段替换导出,要么调整模块结构让引用可动态修改,或者用依赖注入的方式彻底绕过这个问题。
内容的提问来源于stack exchange,提问作者Ivan

