测试时如何覆写模块导入实现mock?能否不使用jest.mock?
不用jest.mock实现模块mock的可行性
你的实现思路在CommonJS(CJS)环境下完全可用,只需要修正代码里的小问题:你导入的foo是模块命名空间对象,不是可执行函数,测试用例里调用foo()必然报错,改成调用你mock的foo.bar()即可。
但如果你的项目已经切换到原生ES Modules(ESM)环境,这种写法会直接失败:ESM规范要求模块导出的绑定是只读的,模块命名空间对象的属性不可被重写,哪怕用Object.defineProperty强制配置可写也会被运行时拦截报错。
jest.mock的运行原理
jest.mock的核心逻辑是模块导入拦截,执行时机远早于业务代码的导入逻辑:
- 首先jest在编译阶段会扫描代码,把所有
jest.mock调用提升到当前模块的最顶部,优先级高于所有import/require语句 - 针对CJS环境:直接修改Node的
require缓存,后续所有导入目标模块的操作,拿到的都是jest返回的mock对象 - 针对ESM环境:通过jest自定义的ESM加载器拦截静态导入请求,替换返回内容为你定义的mock模块
ESM环境下的兼容性
Node过渡到ESM后jest.mock依然可以正常使用,只需要做少量适配配置:
- 在
package.json中设置"type": "module" - jest配置文件中添加
extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx'](根据你使用的后缀调整) - 若使用
ts-jest,需额外在transform配置中开启useESM: true
如果确实不想用标准的jest.mock,ESM环境下可以用jest提供的jest.unstable_mockModule搭配动态导入实现自定义mock,示例如下:
// 先声明mock规则,必须放在动态导入之前 jest.unstable_mockModule('./foo', () => ({ bar: jest.fn() })) describe('foo', () => { let mockFn: jest.Mock let bar: typeof import('./foo').bar beforeEach(async () => { // 动态导入模块,拿到mock后的导出 const fooModule = await import('./foo') bar = fooModule.bar mockFn = bar as jest.Mock }) it('Should not throw', () => { expect(() => bar()).not.toThrow() }) })
内容的提问来源于stack exchange,提问作者David Alsh
相关产品推荐
相关产品推荐

