vue/vitest 项目中常遇到这类问题:vitest 无法直接 mock 同一模块内的内部函数调用(如 `parent` 调用同模块的 `child`),因其导入绑定在模块初始化时已固化;解决方案是将被测副作用函数拆分到独立模块,再通过 `vi.mock()` 精确替换。
在单元测试中,模拟(mock)副作用函数(如日志、API 调用、随机生

✅ 正确做法是遵循「依赖可注入」原则,将副作用逻辑抽离为独立模块:
// 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 child is mocked', () => {
expect(parent()).toBe('foobaz'); // ✅ 通过
});
});⚠️ 关键注意事项:
- vi.mock() 必须置于顶层作用域(不能在 describe 或 it 内部),且需在目标模块被 import 之后、首次使用前执行;
- 若使用 vi.mock('./dummy-child', async (importActual) => {...}) 形式,记得 await importActual() 并显式覆盖所需导出;
- 不要尝试 vi.spyOn(module, 'fn') 来 mock 同模块内函数调用——它只影响通过该引用调用的场景(如 module.child()),对模块内部直调无效;
- 对于无法重构的遗留代码,可考虑使用 vi.hoisted + vi.mock 动态重写,但应视为临时方案。
总结:Vitest 的模块模拟机制基于 ESM 的静态导入图,因此「解耦副作用」是可靠 mock 的前提。将 child 提取为独立模块,不仅使测试可行,也提升了代码的可维护性与关注点分离度。








