vue/vitest 项目中常遇到这类问题:vitest 无法直接 mock 同一模块内的内部函数调用(如 `parent` 调用同模块的 `child`),因其导入绑定在模块初始化时已固化;解决方案是将被测副作用函数拆分到独立模块,再通过 `vi.mock()` 精确替换。
在单元测试中,模拟(mock)副作用函数(如日志、API 调用、随机生成等)是隔离依赖、保证测试确定性的关键。但使用 Vitest 时,一个常见误区是试图直接 vi.spyOn() 或 vi.mock() 当前模块内定义并被同模块其他函数调用的函数——这在 ES 模块环境下必然失败。原因在于:ESM 的导入导出是静态绑定且不可变的,parent() 内部对 child() 的引用指向的是模块作用域内的原始函数,而非后续通过 spyOn 创建的代理。
✅ 正确做法是遵循「依赖可注入」原则,将副作用逻辑抽离为独立模块:
// dummy-child.ts
export function child(): string {
console.log('calling actual child');
return 'bar';
}// dummy-parent.ts
import { child } from './dummy-child';
export function parent(): string {
return `foo${child()}`;
}随后在测试中,使用 vi.mock() 在导入 dummy-parent 之前,为 dummy-child 提供模拟实现:
// dummy.test.ts
import { parent } from './dummy-parent';
// ✅ 在 import 之后、测试前 mock 依赖模块
vi.mock('./dummy-child', () => ({
child: () => 'baz',
}));
describe('parent', () => {
it('should return foobaz when chi
ld is mocked', () => {
expect(parent()).toBe('foobaz'); // ✅ 通过
});
});⚠️ 关键注意事项:
总结:Vitest 的模块模拟机制基于 ESM 的静态导入图,因此「解耦副作用」是可靠 mock 的前提。将 child 提取为独立模块,不仅使测试可行,也提升了代码的可维护性与关注点分离度。