DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
阿里云;从建站到云上业务;云服务器 · 数据库 · 对象存储;查看产品 →
雨云;给项目一台云服务器

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 正确替换,而非删除了需要验证的导入路径。