esm单元测试隔离依赖需借助工具链(如vi.mock())在首次import前拦截模块,路径须带扩展名;避免解构导入,推荐命名空间导入或依赖注入;慎用顶层副作用,改用工厂函数或类封装以提升可测性。

在 ES Modules(ESM)环境中做单元测试时,隔离模块依赖的核心思路是:让被测模块无法直接访问真实依赖,转而使用可控的模拟实现。这和 CommonJS 场景不同——ESM 的 import 是静态、编译期解析的,不能像 require 那样在运行时动态替换。因此,ESM 测试中依赖隔离更依赖工具链支持和代码设计配合。
用 vi.mock() 或 jest.mock() 拦截 ESM 导入
现代测试运行器(Vitest、Jest)已原生支持 ESM 模块拦截。关键在于:必须在模块被首次 import 前调用 mock,并且路径需与被测模块中 import 语句完全一致(包括扩展名)。
- mock 必须写在
import语句之前,否则会被跳过 - 路径要带
.js后缀(如vi.mock('./api.js')),否则可能匹配失败 - 支持自动 mock(
vi.mock('./api.js'))或手动 mock(vi.mock('./api.js', () => ({ fetchUser: vi.fn().mockResolvedValue(...) }))) - 注意:mock 只对后续 import 生效,不会影响已加载的模块(即不支持热替换已导入模块)
避免解构导入,保留属性访问路径
和 CommonJS 类似,ESM 中若对依赖做了命名解构,会切断 mock 的作用链:
- ❌ 错误写法(在被测模块中):
import { fetchUser } from './api.js'; const data = fetchUser();→ 解构后调用的是局部变量,mock 无法接管 - ✅ 推荐写法:
import * as api from './api.js'; const data = api.fetchUser();→ 调用走对象属性访问,mock 可通过重写api.fetchUser生效 - 或者改用默认导出 + 工厂函数:
import createApi from './api.js'; const api = createApi(config);,测试时传入 mock 实现
用依赖注入替代顶层导入
把依赖从模块顶层移开,改为函数参数或构造函数参数,是最可靠、最易测试的设计方式:
- 被测函数不再
import任何外部模块,而是接收服务实例作为参数 - 例如:
function loadProfile(userService) { return userService.getProfile(); } - 测试时直接传入 mock 对象:
loadProfile({ getProfile: vi.fn().mockResolvedValue(...) }) - 适用于工具函数、业务逻辑层,也便于组合和复用
慎用顶层副作用,拆分可替换边界
ESM 模块顶层代码会在 import 时立即执行,一旦包含真实 I/O(如初始化 client、读取 config),就难以隔离:
- ❌ 避免:
const client = createHttpClient(); export async function request() { return client.get(...); } - ✅ 改为工厂模式:
export function createRequester(client) { return async function request() { ... }; } - 或封装成类:
export class ApiService { constructor(client) { this.client = client; } },测试时 new 一个带 mock client 的实例
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











