Jest ESM模式下mock导入函数报只读属性/常量赋值错误
Jest ESM模式下mock导入函数报错解决
问题现象
在ESM模式下使用Jest mock导入的工具函数时,会触发两类报错:
TypeError: Assignment to constant variable.TypeError: Cannot assign to read only property 'sum' of object '[object Module]'
预期效果为测试用例中mock函数可返回自定义设置的返回值。
已尝试的无效方案
尝试1
import { jest } from '@jest/globals' import * as util from "./util.js" it("TypeError: Cannot assign to read only property 'sum' of object '[object Module]'", () => { jest.spyOn(util, "sum").mockImplementation(() => { return 2 }) expect(sum(1, 2)).toBe(2); })
运行后触发只读属性赋值报错。
尝试2
import { jest } from '@jest/globals' import { sum } from './util.js' it("TypeError: Cannot assign to read only property 'sum' of object '[object Module]'", () => { jest.mock("./util.js", () => ({ __esModule: true, sum: jest.fn().mockReturnValue(2), })); expect(sum(1, 2)).toBe(2); })
运行后触发只读属性赋值报错。
尝试3
import { jest } from '@jest/globals' import { sum } from "./util.js" it("TypeError: Assignment to constant variable.", () => { sum = jest.fn(() => { return 2 }) expect(sum(1, 2)).toBe(2); })
运行后触发常量赋值报错。
现有配置
已按照Jest官方ESM指引完成基础配置:
package.json
{ "type": "module", "scripts": { "test": "NODE_OPTIONS=--experimental-vm-modules jest" } }
jest.config.js
module.exports = async () => { return { verbose: true, transform: {} }; };
可复现问题的仓库地址:https://github.com/fei1990a/jest-esm/tree/main
错误原因
ESM模块规范和CommonJS不同,静态导入的模块命名空间对象、具名导入都是只读绑定,Jest无法直接修改:
- 静态
import拿到的模块是原生只读的Module Namespace Object,直接修改属性、调用spyOn都会触发只读错误 - ESM下具名导入是和原模块强绑定的常量,直接给导入的变量赋值会触发常量赋值错误
jest.mock/jest.unstable_mockModule只会被提升到文件顶层执行,写在测试用例的块级作用域内不会生效,执行到对应代码时真实模块已经加载完成- 静态import的执行优先级高于所有业务代码,就算在文件顶层写了mock,只要提前静态import了对应模块,拿到的还是未被mock的真实导出
- 尝试1额外存在作用域问题:代码中从未在当前作用域导入
sum变量,就算mock执行成功,直接调用sum()也会触发变量未定义的引用错误
正确写法
方案1:全量mock模块
使用官方提供的ESM专用mock API jest.unstable_mockModule,所有被mock的模块必须在mock声明后用动态import()导入:
import { jest } from '@jest/globals' // mock声明写在文件顶层,会被自动提升到所有静态import之后、动态import之前执行 jest.unstable_mockModule('./util.js', () => ({ __esModule: true, sum: jest.fn(() => 2) })) it('mock sum返回指定值', async () => { // 等mock生效后再动态导入模块 const { sum } = await import('./util.js') expect(sum(1, 2)).toBe(2) })
方案2:spyOn局部mock单个方法
如果只想mock模块里的部分方法,保留其他方法的真实逻辑,用动态导入配合spyOn实现即可:
import { jest } from '@jest/globals' it('spyOn mock sum方法', async () => { // 动态导入拿到Jest代理过的模块对象,支持属性改写 const util = await import('./util.js') const spySum = jest.spyOn(util, 'sum').mockImplementation(() => 2) // 注意调用的时候要取模块对象上的sum,不要用静态导入的sum expect(util.sum(1, 2)).toBe(2) // 测试完成后恢复原实现,避免影响其他用例 spySum.mockRestore() })
内容的提问来源于stack exchange,提问作者Feizhang210291
相关产品推荐
相关产品推荐

