You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

使用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.jsonmoduleResolution设置为"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的工作机制,我整理了几个可能的修复方向:

  1. 调整模块导入顺序
    你在测试文件顶部先静态导入了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)
  })
})
  1. 检查moduleNameMapper的影响
    你的moduleNameMapper配置了"^(.*)\\.js$": "$1",这可能在ESM模式下干扰模块路径解析——ESM要求必须带.js后缀,但这个映射会去掉后缀,可能导致Jest无法正确识别模块的真实路径,进而让模拟失效。

尝试调整:暂时注释掉这个映射,或者改成更精确的规则,比如:

moduleNameMapper: {
  // "^(.*)\\.js$": "$1",
  "^@/(.*)$": "<rootDir>/src/$1", // 如果项目有别名的话用这个替代
},
  1. 启用模块缓存重置
    Jest的模块缓存可能导致模拟状态无法更新,可以在jest.config.ts中添加以下配置,确保每个测试用例都重置模块缓存、恢复模拟状态:
resetModules: true,
restoreMocks: true,
  1. 确认tsconfig和jest配置的一致性
    确保tsconfig.json中的module字段设置为"NodeNext""ESNext",和moduleResolution保持一致,这样Jest和TypeScript的模块解析逻辑才能对齐。

备注:内容来源于stack exchange,提问作者wmash

火山引擎 最新活动