Vitest 中如何模拟自定义指令使用的 ResizeObserver
结论
报错是因为 @abc/my-common-ui 模块执行时会直接调用全局的 ResizeObserver。vi.mock() 工厂中的 ResizeObserver 只会作为模拟模块的导出值返回。工厂会先执行 importOriginal(),原模块因此在模拟导出返回前就开始求值。此时如果全局没有 ResizeObserver,替换模块导出也无法解决原模块求值时的报错。
在 Vitest 测试环境中,应先设置全局 ResizeObserver,再加载依赖它的模块。如果还需要模拟 @abc/my-common-ui 的其他导出,可以等全局替身就绪后再进行部分模拟。
在测试设置文件中提供全局替身
在 test-setup.ts 中设置全局对象:
globalThis.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
} as any;
这段设置必须在导入或加载 @abc/my-common-ui 及相关组件之前运行。Vitest 会先执行配置的 setup 文件,再加载测试文件。不过,如果 test-setup.ts 顶部静态导入了依赖这个全局对象的模块,该模块仍可能在赋值前求值。
也可以使用 Vitest 的全局替换 API:
import { vi } from 'vitest';
vi.stubGlobal('ResizeObserver', class {
observe() {}
unobserve() {}
disconnect() {}
});
替换同样要放在相关模块加载之前。如果测试需要断言这些方法是否被调用,可以将它们改为 vi.fn():
import { vi } from 'vitest';
vi.stubGlobal('ResizeObserver', class {
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
});
需要部分模拟模块时
设置好全局替身后,原模块的顶层代码就能找到 ResizeObserver。这时再调用 importOriginal(),原模块才可能顺利加载:
vi.mock('@abc/my-common-ui', async importOriginal => {
const originalModule = await importOriginal();
return {
...originalModule,
SvgCacheService: vi.fn(),
};
});
如果原模块导出了名为 ResizeObserver 的值,也可以覆盖这个导出。不过,如果模块内部使用的是全局 ResizeObserver,覆盖导出仍无法代替全局设置。是否保留对 @abc/my-common-ui 的模拟,取决于测试是否还要替换它的其他导出。
关于静态导入和 ESLint 警告
全局替身必须先于依赖模块执行。只要 Vitest 的 setup 文件在测试文件之前运行,测试文件通常仍可使用静态导入。如果项目的 ESLint 规则禁止静态导入某些懒加载库,说明该规则会检查相应的导入。检查被标记的导入及其加载方式,再按项目规则处理。
备注:内容仅供参考。
