目标
vi.mock 会被提升到所有 import 之前执行,好让替身在模块图加载前就位;代价是闭包语义:工厂引用的任何文件级变量都会在初始化之前被捕获,读出来是 undefined。理解这个执行顺序后,“mock 返回 undefined”“工厂在 import 时抛错”都不再是玄学,而是可以推导的结果。
步骤要点
- 用最小文件复现空壳 mock:把变量声明放在 vi.mock 之后,断言失败显示 undefined,这就是提升捕获的典型症状。
- 加三行 console 标记确认执行顺序:工厂先于文件体、文件体先于 import 之后,B-A-C 的日志铁证如山。
- 两种修法择一:用 state 对象让工厂在调用时惰性读取,或用 vi.hoisted 把工厂真正需要的值与工厂一起提升。
- 挑对 mock 边界:有 import 副作用的模块(如导入即建连接池)才是该替身的那一层,mock 上层会让副作用活着把测试挂死。
- 加一条“替身确实被调用过”的断言(toHaveBeenCalledTimes),证明测试跑在真实接线之上而不是空对象旁边。
排错提示
工厂在 import 时抛“不是函数”多半引用了尚未初始化的外层辅助函数,把它挪进 vi.hoisted 或直接内联;部分 mock 破坏模块其余导出时,工厂里用 importOriginal 展开真实导出再覆盖需要的那部分;同一 mock 在两个文件表现不同是说明符解析差异,vi.mock 必须写测试文件里 import 时的确切相对路径而非规范化绝对路径。
适用边界
vi.mock 对测试启动后才解析的动态 import 并非总能拦截,边界模块优先静态 import;提升机制好用但有锋利边缘,关键模块改用朴素的依赖注入缝隙可完全绕开它。
代码示例
// What the runner actually executes:
vi.mock('./db', () => ({ query: () => FAKE })); // 1. hoisted first
import { getUser } from './user'; // 2. imports (mock applies)
const FAKE = { id: 1 }; // 3. far too late
// The two real fixes:
const { FAKE } = vi.hoisted(() => ({ FAKE: { id: 1 } }));
vi.mock('./db', () => ({ query: () => FAKE }));
// or read lazily:
const state = { fake: null as unknown };
vi.mock('./db', () => ({ query: () => state.fake }));