在Node.js项目的单元测试场景中,Vitest是常用的测试框架,其提供的vi.mock方法用于模拟模块依赖,但在CommonJS模块环境下,不少开发者会遇到vi.mock不生效的情况,模拟的模块没有被正确替换,导致测试用例执行不符合预期。

问题常见触发场景
首先我们需要明确哪些情况容易出现vi.mock在CommonJS环境失效的问题:
- 项目使用CommonJS规范编写,没有配置ES模块相关参数,直接使用默认的vi.mock语法
- 被模拟的模块导出方式是module.exports,而模拟时使用了ES模块的导出写法
- 模拟的文件路径和实际引入路径不匹配,或者文件后缀处理不符合CommonJS的解析规则
- 在测试文件中没有正确声明模块环境,Vitest默认按ES模块处理导致模拟失效
核心解决方法
1. 明确声明测试环境为CommonJS
需要在Vitest的配置文件或者测试文件的头部明确指定模块环境,避免Vitest默认按ES模块解析。如果是全局配置,可以在vitest.config.js中添加如下配置:
// vitest.config.js
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// 指定环境为node,同时设置模块系统为commonjs
environment: 'node',
// 对于CommonJS项目,可关闭ES模块转换
transformMode: {
web: [],
ssr: []
}
}
})
如果是单个测试文件需要适配,可以在文件头部添加注释声明:
// @vitest-environment node
// 声明当前文件使用CommonJS模块解析
const { vi, test, expect } = require('vitest')
const path = require('path')
2. 调整vi.mock的模拟写法适配CommonJS
CommonJS模块的导出是module.exports对象,因此模拟时需要匹配对应的导出结构,不能直接使用ES模块的default导出写法。假设我们有一个被模拟的CommonJS模块utils.js:
// utils.js 被模拟的CommonJS模块
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
}
正确的模拟写法应该是:
// test.spec.js
const { vi, test, expect } = require('vitest')
// 模拟utils模块,返回的对象需要和原模块的module.exports结构一致
vi.mock('./utils', () => {
return {
add: vi.fn((a, b) => a + b),
subtract: vi.fn((a, b) => a - b)
}
})
const utils = require('./utils')
test('模拟utils的add方法', () => {
expect(utils.add(1, 2)).toBe(3)
expect(utils.add).toHaveBeenCalledWith(1, 2)
})
如果原模块是单个导出,比如:
// helper.js 单个导出的CommonJS模块 module.exports = (a, b) => a * b
模拟时也需要返回对应的函数:
vi.mock('./helper', () => {
// 直接返回函数,匹配原模块的module.exports
return vi.fn((a, b) => a * b)
})
3. 检查模块路径和后缀匹配
CommonJS的模块解析规则和ES模块不同,如果引入模块时没有写后缀,Vitest可能会优先查找.js文件,但如果项目中使用的是.cjs后缀,需要明确指定路径。例如被模拟的文件是utils.cjs,引入时必须写全路径:
// 错误写法,可能找不到模块导致模拟失效
vi.mock('./utils', () => {})
// 正确写法,匹配CommonJS的文件后缀
vi.mock('./utils.cjs', () => {})
4. 避免模拟时机错误
vi.mock的调用必须放在测试文件的顶层,不能放在test或者beforeEach等函数内部,否则Vitest无法在模块加载阶段完成模拟替换。错误的写法示例:
const { vi, test, expect } = require('vitest')
test('错误示例', () => {
// 错误:不能在test内部调用vi.mock
vi.mock('./utils', () => {})
const utils = require('./utils')
})
正确的写法是将vi.mock放在文件顶部:
const { vi, test, expect } = require('vitest')
// 正确:顶层调用vi.mock
vi.mock('./utils', () => {})
test('正确示例', () => {
const utils = require('./utils')
expect(utils.add).toBeDefined()
})
排查步骤总结
如果遇到vi.mock在CommonJS环境不生效的问题,可以按照以下步骤排查:
- 检查Vitest配置是否正确声明了CommonJS环境,没有开启不必要的ES模块转换
- 检查vi.mock的返回值是否和被模拟模块的module.exports结构一致
- 检查模拟的模块路径是否和实际引入路径完全匹配,包括后缀名
- 检查vi.mock是否放在测试文件顶层,没有嵌套在测试用例内部
- 可以通过在模拟函数中添加console.log,确认模拟函数是否被执行,判断是模拟未生效还是测试用例逻辑问题
注意:如果项目中同时存在CommonJS和ES模块,建议统一模块规范,或者明确划分不同模块的解析规则,避免混合使用导致的各类兼容问题。