Vitest 5 的 mock 偶尔失效:等待异步 vi.hoisted
异步 vi.hoisted 内启动导入,却只在断言中等待结果,可能让同文件的 mock 没有按预期生效。将等待放回提升阶段。
同一个测试单独运行正常,放进整组后却偶尔读到真实模块;vi.mock() 明明写在文件里,断言仍显示 expected 'real' to be 'mocked'。如果 vi.hoisted(async () => ...) 内启动了动态 import,先检查是否等待了整个提升调用。
这条现象在 Vitest 5.0.1 中出现,旧 4.1.11 的同一结构可能没有暴露。需要调整的是异步初始化顺序:只在后面的测试断言里 await helper,并不能要求 mock 登记阶段等待它。
await 应放在 vi.hoisted 外面
下面是一个测试文件的结构示例。helper.js 在本例中应导出同步的 makeHelper(),返回 { tag: 'helper' };dep.js 应导出 value;文件路径需对应项目真实模块。
import { expect, test, vi } from 'vitest';
import { value } from './dep.js';
const helper = await vi.hoisted(async () => {
const { makeHelper } = await import('./helper.js');
return makeHelper();
});
vi.mock('./dep.js', () => ({ value: 'mocked' }));
test('uses the registered mock', () => {
expect(helper).toEqual({ tag: 'helper' });
expect(value).toBe('mocked');
});
这里等待发生在提升工厂调用处,后续 mock 与被测模块加载按转换后的顺序继续。helper.js 是提升阶段使用的辅助模块,不应再提前导入待 mock 的 dep.js 或被测模块;否则它可能在 mock 登记前就进入缓存,形成另一个提前加载问题。
vi.hoisted 返回工厂本身的返回值。工厂是 async 时,这个返回值是 Promise,不会因为变量名叫 helper 就变成已经完成的对象。当前正式 5.0.5 的 API 示例同样使用 await vi.hoisted(async () => ...);这里无需等一个特定“修复版本”来表达正确顺序。
没有提前异步工作的需求,就保持同步
如果提升阶段只是创建 mock 函数或固定数据,可使用同步工厂:
const mocks = vi.hoisted(() => ({ read: vi.fn() }));
不要为了复用一个简单对象把整条业务依赖链导入提升阶段。确实需要异步准备时,应等待该阶段,或把不影响 mock 登记的工作延后到正常测试生命周期,再明确它的依赖关系。两种方式的执行时机不同,不能机械替换。
把 vi.mock 在源码里往前挪几行,不能代替这个 await;Vitest 会提升并重写相关语句,源码视觉顺序不等于最终加载顺序。换线程池、关隔离或给断言加 sleep,也不能建立初始化完成的约束。
回归要保留原来的文件并发规模、隔离设置和所用 pool,先确认被测模块拿到的是 mock,再检查真正的业务断言。单文件通过只能证明这一种调度没有暴露问题。若使用 server-only 等真实模块会主动抛错的依赖,还应确认错误消失来自 mock 正确替换,而非删除了需要验证的导入路径。


