PAYATHON 2026

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 规则禁止静态导入某些懒加载库,说明该规则会检查相应的导入。检查被标记的导入及其加载方式,再按项目规则处理。

备注:内容仅供参考。