使用Jest的unstable_mockModule无法正确模拟ESM模块的问题求助
使用Jest的unstable_mockModule无法正确模拟ESM模块的问题求助
我在项目中使用ESM规范,所有.ts文件都会被转译到dist目录下。现在遇到一个棘手的问题:用Jest的unstable_mockModule模拟src/store/index.js模块时,测试文件里能拿到模拟后的版本,但实际被测试的main.ts里却还是调用了原模块的getScalpRunners方法,完全没生效。以下是我的项目细节和配置,麻烦帮忙看看哪里出问题了?
项目目录结构
dist/ ├─ src/ │ ├─ store/ │ │ ├─ index.js │ ├─ strategies/ │ │ ├─ scalping/ │ │ │ ├─ main.js src/ ├─ store/ │ ├─ index.ts ├─ strategies/ │ ├─ scalping/ │ │ ├─ main.ts test/ ├─ strategies/ │ ├─ scalping/ │ │ ├─ main.test.ts
核心配置
我的tsconfig.json中moduleResolution设置为"NodeNext",jest.config.ts配置如下:
import type { Config } from "jest" const config: Config = { preset: 'ts-jest/presets/default-esm', displayName: "scalper", testTimeout: 60000, // 30 secs - Necessary for when testing timers globalSetup: "<rootDir>/test/jest/setup.ts", globalTeardown: "<rootDir>/test/jest/teardown.ts", coveragePathIgnorePatterns: [ "/node_modules/" ], testMatch: ["<rootDir>/test/**/?(*.)+(spec|test).[jt]s?(x)"], modulePathIgnorePatterns: ["<rootDir>/test/.*/__mocks__", "<rootDir>/dist/"], testEnvironment: "node", coverageDirectory: "<rootDir>/coverage", extensionsToTreatAsEsm: [".ts"], moduleNameMapper: { "^(.*)\\.js$": "$1", }, transform: { '^.+\\.ts?$': [ 'ts-jest', { useESM: true } ] }, } export default config
测试文件代码(main.test.ts)
import { jest } from '@jest/globals' import * as fs from "fs" import * as path from "path" import appRoot from "app-root-path" const params = JSON.parse(fs.readFileSync(path.join(appRoot.path, 'test/strategies/scalping/main/params.json'), 'utf8')) import * as strategies from "../../../../src/strategies/scalping/index.js" describe('main', () => { it('Should create a valid scalp trade', async () => { const { builtMarket, config } = params // 按照ESM模拟文档尝试动态导入方式 jest.unstable_mockModule('../../../../src/store/index.js', () => ({ getScalpRunners: jest.fn().mockResolvedValue(params as never) })) const store = await import('../../../../src/store/index.js') await strategies.scalping(builtMarket, config) }) })
被测试文件代码片段(main.ts)
import store from "../../store/index.js" export default async function(builtMarket: BuiltMarket, config: Config): Promise<BuiltMarket | undefined> { try { const validMarket = isValidMarket(builtMarket, config) if (validMarket) { const polledRunners: ScalpRunner[] | undefined = await store.getScalpRunners({ eventId: builtMarket._eventId, marketId: builtMarket._marketId }) // 其他业务逻辑... } } catch(err: any) { throw err } }
问题现象
我在测试文件里打印store,确实是模拟后的版本,但main.ts里的store却始终是原模块,导致getScalpRunners调用的是真实实现,完全没用到模拟的方法。我已经参考了Jest官方关于ESM模块模拟的文档,也查过相关问题,但还是没解决。
可能的原因和解决方案
结合ESM模块的特性和Jest的工作机制,我整理了几个可能的修复方向:
- 调整模块导入顺序
你在测试文件顶部先静态导入了strategies模块,之后才去模拟store。在ESM中,模块是静态解析的,当strategies被导入时,它会立即加载依赖的store模块,这时候模拟还没生效,所以strategies里的store是原模块。
修复方法:把strategies的导入放到模拟之后,改用动态导入:
// 移除顶部的静态导入 // import * as strategies from "../../../../src/strategies/scalping/index.js" describe('main', () => { it('Should create a valid scalp trade', async () => { const { builtMarket, config } = params jest.unstable_mockModule('../../../../src/store/index.js', () => ({ getScalpRunners: jest.fn().mockResolvedValue(params as never) })) // 先完成模拟,再动态导入策略模块 const store = await import('../../../../src/store/index.js') const strategies = await import('../../../../src/strategies/scalping/index.js') await strategies.scalping(builtMarket, config) }) })
- 检查moduleNameMapper的影响
你的moduleNameMapper配置了"^(.*)\\.js$": "$1",这可能在ESM模式下干扰模块路径解析——ESM要求必须带.js后缀,但这个映射会去掉后缀,可能导致Jest无法正确识别模块的真实路径,进而让模拟失效。
尝试调整:暂时注释掉这个映射,或者改成更精确的规则,比如:
moduleNameMapper: { // "^(.*)\\.js$": "$1", "^@/(.*)$": "<rootDir>/src/$1", // 如果项目有别名的话用这个替代 },
- 启用模块缓存重置
Jest的模块缓存可能导致模拟状态无法更新,可以在jest.config.ts中添加以下配置,确保每个测试用例都重置模块缓存、恢复模拟状态:
resetModules: true, restoreMocks: true,
- 确认tsconfig和jest配置的一致性
确保tsconfig.json中的module字段设置为"NodeNext"或"ESNext",和moduleResolution保持一致,这样Jest和TypeScript的模块解析逻辑才能对齐。
备注:内容来源于stack exchange,提问作者wmash




