Vitest 中 spyOn 必须在测试作用域内声明:原因与配置冲突解析

vitest 的 `vi.spyon()` 无法在 `describe` 外部全局声明,因其依赖于 vitest 的自动 mock 重置机制;当启用 `mockreset`、`restoremocks` 或 `clearmocks` 等配置时,全局 spy 会在每个测试前被清除,导致断言失败。

在从 Jest 迁移至 Vitest 的过程中,开发者常会沿用 Jest 的“外部声明 spy”习惯(如在 describe 顶层定义 const spy = vi.spyOn(...)),但该模式在 Vitest 中默认不兼容——根本原因在于 Vitest 的 mock 生命周期管理策略与 Jest 存在关键差异。

Vitest 默认启用严格的 mock 隔离机制。当你在 vitest.config.ts 中配置了以下任一选项:

test: {
  mockReset: true,     // 每个测试前调用 mock.reset()
  restoreMocks: true,  // 每个测试后还原所有 mock(含 spyOn)
  clearMocks: true,    // 每个测试前清空 mock 调用记录和返回值
  threads: false,      // 单线程模式下 mock 状态更易受干扰(虽非主因,但加剧问题)
}

Vitest 就会在 每个 it() 执行前后主动干预 mock 状态

  • restoreMocks: true 会将 vi.spyOn() 创建的 spy 还原为原始方法(即取消监听);
  • clearMocks: true 会清空 .mock.calls、.mock.results 等内部状态;
  • 若 spy 在 describe 外声明,则其引用指向的已是被还原/清空后的“空壳”,后续 expect(spy).toHaveBeenCalledOnce() 必然失败。

✅ 正确实践:始终在 it() 或 beforeEach() 内创建 spy
这是最符合 Vitest 设计哲学的方式,确保每个测试拥有独立、干净的 spy 实例:

describe('PostboxList', () => {
  it('shows notification when fetching status is HasError', async () => {
    // ✅ 正确:spy 属于当前测试生命周期
    const notificationSpy = vi.spyOn(NotificationActions, 'addNotification');

    const store = mockStore({
      postbox: {
        documents: { data: [], fetchingStatus: DataFetchingStatus.HasError },
        messages: { data: [], fetchingStatus: DataFetchingStatus.HasError },
      },
    });

    render(, { store });

    expect(notificationSpy).toHaveBeenCalledOnce({
      title: 'POSTBOX.ERROR.TITLE',
      text: 'POSTBOX.ERROR.TEXT',
    });
  });
});

⚠️ 注意事项:

  • 不要依赖 beforeAll() 声明 spy —— 它仍会受 restoreMocks/clearMocks 影响;
  • 如需复用 spy 创建逻辑,可封装为工厂函数,而非提前实例化:
    const createNotificationSpy = () => vi.spyOn(NotificationActions, 'addNotification');
    // 然后在 each it() 中调用:const spy = createNotificationSpy();
  • 若必须保留全局 spy(极少数场景),请显式禁用相关配置
    test: {
      mockReset: false,
      restoreM

    ocks: false, clearMocks: false, // threads: false 可保留(单测调试友好),但需自行管理 mock 状态 }

    ⚠️ 此方式牺牲测试隔离性,易引发跨测试污染,强烈不推荐用于 CI 或大型测试套件

总结:Vitest 的 spyOn 是“测试作用域绑定”的轻量级监控工具,其行为由框架的 mock 生命周期严格管控。迁移时应主动拥抱这一设计——将 spy 创建移入测试内部,既是解决报错的直接方案,更是保障测试健壮性与可维护性的最佳实践。