
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, restoreMocks: false, clearMocks: false, // threads: false 可保留(单测调试友好),但需自行管理 mock 状态 }⚠️ 此方式牺牲测试隔离性,易引发跨测试污染,强烈不推荐用于 CI 或大型测试套件。
总结:Vitest 的 spyOn 是“测试作用域绑定”的轻量级监控工具,其行为由框架的 mock 生命周期严格管控。迁移时应主动拥抱这一设计——将 spy 创建移入测试内部,既是解决报错的直接方案,更是保障测试健壮性与可维护性的最佳实践。










