Vitest mock 类字段后报 __vi_import_0__ 未初始化,检查同名导入
Vitest 4.1.3 mock 类的实例字段与导入同名时,可能提前读取 __vi_import_0__。以固定计算键局部避开,并核对 4.1.4 转换修复。
测试还没进入断言,就报 ReferenceError: Cannot access '__vi_import_0__' before initialization,错误位置却指向 vi.hoisted() 里的 vi.fn()。如果 mock 工厂返回一个带实例字段的类,而字段名恰好与文件中的导入同名,应查看转换后的代码,避免把工具生成的错误当成业务循环依赖。
这一具体回归在 Vitest 4.1.3、TypeScript 6.0.2 且启用 useDefineForClassFields 的组合中确认,Vitest 4.1.4 已修复。它不意味着所有初始化前访问都由 Vitest 引起;真实的循环依赖和 mock 工厂读取尚未初始化变量,仍须分别排查。
字段名被误当成了变量引用
下面是测试文件中的结构片段,./MyClass 与 ./my-method 代表项目已有模块:
import { vi } from 'vitest';
import { myMethod } from './my-method';
const { myMethodMock } = vi.hoisted(() => ({
myMethodMock: vi.fn(),
}));
vi.mock('./MyClass', () => ({
MyClass: class {
myMethod = myMethodMock;
},
}));
类里的左侧 myMethod 是属性名,右侧 myMethodMock 才是它使用的变量。4.1.3 的转换错误地把前者联系到导入的 myMethod,额外生成了一次提前读取:
const myMethod = __vi_import_0__.myMethod;
// 此处之后才登记 mock、初始化 __vi_import_0__。
这个内部变量此时还没有初始化,因而落入暂时性死区。源码的行列号来自转换映射,未必精确指向真正错误的生成语句。先比较实际类字段、导入名称及生成顺序,比给 vi.fn() 外面加延时更有用。
暂时用计算属性名保留实例字段
在无法立即更新工具链时,可将这一个静态字段写为等价的字符串计算键:
vi.mock('./MyClass', () => ({
MyClass: class {
['myMethod'] = myMethodMock;
},
}));
它仍然创建实例自有字段,字段值仍是同一个 mock 函数,避免了这条错误的标识符分类。这里只适用于固定字符串字段;若计算键本身读取导入变量,当然仍要按真实求值顺序处理。
改成 myMethod() { return myMethodMock(); } 也可能避开这个故障,但会把实例字段改成原型方法,并改变函数身份等行为。原测试若依赖实例自有属性或函数引用,不宜只为绕过转换错误机械替换。全局关闭 useDefineForClassFields 同样影响真实类字段语义,范围远大于这一处 mock。
修复版本会区分属性键与初始值
Vitest 4.1.4 的正式转换逻辑不再把非计算类字段的名称当成引用,同时保留对字段初始值中真实导入引用的处理。升级时核对工作区实际解析到的 Vitest 与相关 mocker 包,不要只替换编辑器使用的 TypeScript。
保存旧锁文件与测试后,在允许的测试环境重新执行原失败文件。验收需要进入真正的被测函数,确认它调用的是登记的 mock、参数与返回值符合原预期;仅没有初始化异常,可能是 mock 根本没有生效。
若更新后仍失败,继续查看是否存在工厂闭包读取外部未初始化变量、setup 提前导入或实际循环依赖。不要删除所有 import 再用延时导入兜底,那会改变模块初始化顺序,反而掩盖需要测试的行为。


