You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从Jest迁移到Vitest:spyOn在测试用例外部失效问题

问题:Vitest中全局声明的spyOn无法正常工作,移至测试用例内部则恢复正常

现象

将vi.spyOn声明在测试用例作用域之外时,断言spy的调用情况会失败:

const notificationSpy = vi.spyOn(NotificationActions, 'addNotification');

describe('PostboxList', () => {
    const renderComponent = (store: Store) => {
        render(<PostboxList />, {
            store,
        });
    };

    it('the notification is visible when fetching status is HasError', async () => {
        const store = mockStore({
            postbox: {
                documents: {
                    data: [],
                    fetchingStatus: DataFetchingStatus.HasError,
                },
                messages: {
                    data: [],
                    fetchingStatus: DataFetchingStatus.HasError,
                },
            },
        });

        renderComponent(store);

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

但将vi.spyOn移到测试用例(it块)内部后,测试就能通过:

describe('PostboxList', () => {
    const renderComponent = (store: Store) => {
        render(<PostboxList />, {
            store,
        });
    };

    it('the notification is visible when fetching status is HasError', async () => {
        const notificationSpy = vi.spyOn(NotificationActions, 'addNotification');

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

        renderComponent(store);

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

当前Vitest配置的test选项:

test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './vitest.setup.ts',
    include: ['**/*.{test, spec}.{ts,tsx,js,jsx}'],
    exclude: [...configDefaults.exclude, 'plop', './vitest.setup.ts'],
    deps: {
        inline: ['vitest-canvas-mock'],
    },
    testTimeout: 10000,
    threads: false,
    alias: tsconfigPaths(),
    mockReset: true,
    restoreMocks: true,
    clearMocks: true,
    css: true,
}

已尝试在beforeEach/afterEach中手动重置mock和spy,但未解决问题。

原因分析

你配置中的restoreMocks: true和clearMocks: true会在每个测试用例执行前后自动重置/清除mock:

  • restoreMocks: true会在每个测试后将spy恢复为原始实现,同时清除调用记录
  • clearMocks: true会在每个测试前清除mock的调用记录

当spy声明在全局作用域时,初始化阶段创建的spy会被这些自动重置操作影响:要么调用记录被提前清空,要么spy被恢复为原始方法,导致断言时无法捕获到目标调用。而将spy放在测试用例内部时,每次测试都会重新创建spy,避开了全局重置的影响,确保断言时使用的是当前测试周期内有效的spy实例。

解决方案

  1. 保持spy在测试用例内部声明:这是最稳妥的方式,确保每个测试用例都使用独立的spy实例,完全避开全局重置机制的干扰。

  2. 使用beforeEach批量创建spy:如果多个测试用例需要复用同一个spy逻辑,可以在beforeEach中创建,确保每个测试用例执行前都生成新的spy实例:

    describe('PostboxList', () => {
        let notificationSpy: ReturnType<typeof vi.spyOn>;
    
        beforeEach(() => {
            notificationSpy = vi.spyOn(NotificationActions, 'addNotification');
        });
    
        // ...测试用例
    });
    
  3. 调整Vitest配置(不推荐):如果一定要在全局作用域声明spy,可以关闭自动重置相关选项,但需手动管理mock生命周期避免测试间状态污染:

    test: {
        // ...其他配置
        mockReset: false,
        restoreMocks: false,
        clearMocks: false,
    }
    

    同时在afterEach中手动重置spy:

    afterEach(() => {
        notificationSpy.mockReset();
    });
    

内容的提问来源于stack exchange,提问作者cassmtnr

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 11:17:03