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

从Jest迁移到Vitest后,useNavigate的mockNavigate未被调用问题

从Jest迁移到Vitest时useNavigate mock不触发的问题

问题场景

我正从Jest迁移到Vitest,在测试中检查useNavigate返回的navigate()函数是否被调用时遇到问题,断言始终失败,提示mock函数从未被调用。

vitest setup.ts 代码

import matchers from '@testing-library/jest-dom/matchers';
import { expect, vi } from 'vitest';

expect.extend(matchers);

export const mockNavigate = vi.fn();

vi.mock('react-router-dom', async () => {
    const router = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
    return {
        ...router,
        useNavigate: vi.fn().mockReturnValue(mockNavigate),
    };
});

组件测试代码

import React from 'react';
import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { SigninComponent } from '../../../../../../src/features/auth/components/signin.component';
import { renderWithProviders } from '../../../../utils';
import { mockNavigate } from '../../../../setup';

describe('Signin Component', () => {
   it('navigates to signup page', async () => {
        renderWithProviders(<SigninComponent />);
   
        userEvent.click(screen.getByTestId('test-nav-btn'));
        await screen.findByText('AppName');

        expect(mockNavigate).toHaveBeenCalledWith('/signup');
    });
});

带Providers的渲染辅助函数

import React, { ReactNode } from 'react';
import { render } from '@testing-library/react';
import { gqlClient } from '../../src/common/gql/gql-auth';
import { Provider } from 'react-redux';
import { ApolloProvider } from '@apollo/client';
import { MemoryRouter } from 'react-router-dom';
import { store } from '../../src/common/state/store';

export const renderWithProviders = (component: ReactNode) => {
    return render(
        <Provider store={store}>
            <ApolloProvider client={gqlClient}>
                <MemoryRouter>{component}</MemoryRouter>
            </ApolloProvider>
        </Provider>,
    );
};

遇到的错误

AssertionError: expected "spy" to be called with arguments: [ '/signup' ]
Received:
Number of calls: 0


修复方案

1. 调整导入顺序,确保mock先生效

Vitest的模块mock需要在被mock的模块(react-router-dom)被组件导入前完成初始化。修改测试文件的导入顺序,把setup文件的导入放在组件导入之前:

// 先导入setup,确保mock优先初始化
import { mockNavigate } from '../../../../setup';
// 再导入组件
import { SigninComponent } from '../../../../../../src/features/auth/components/signin.component';
// 其他导入...

2. 解决MemoryRouter覆盖mock的问题

renderWithProviders中使用的MemoryRouter会提供真实的路由上下文,覆盖你全局mock的useNavigate。可以修改渲染辅助函数,支持传入自定义的navigate mock:

import React, { ReactNode } from 'react';
import { render } from '@testing-library/react';
import { gqlClient } from '../../src/common/gql/gql-auth';
import { Provider } from 'react-redux';
import { ApolloProvider } from '@apollo/client';
import { MemoryRouter } from 'react-router-dom';
import { store } from '../../src/common/state/store';
import { vi } from 'vitest';

// 自定义路由Provider,用于mock useNavigate
const MockRouterProvider = ({ children, navigateMock }: { children: ReactNode, navigateMock?: () => void }) => {
  vi.spyOn(require('react-router-dom'), 'useNavigate').mockReturnValue(navigateMock || vi.fn());
  return <MemoryRouter>{children}</MemoryRouter>;
};

export const renderWithProviders = (component: ReactNode, options?: { navigateMock?: () => void }) => {
    return render(
        <Provider store={store}>
            <ApolloProvider client={gqlClient}>
                <MockRouterProvider navigateMock={options?.navigateMock}>
                    {component}
                </MockRouterProvider>
            </ApolloProvider>
        </Provider>,
    );
};

然后在测试中使用这个修改后的辅助函数:

describe('Signin Component', () => {
   it('navigates to signup page', async () => {
        const mockNavigate = vi.fn();
        renderWithProviders(<SigninComponent />, { navigateMock: mockNavigate });
   
        await userEvent.click(screen.getByTestId('test-nav-btn'));
        expect(mockNavigate).toHaveBeenCalledWith('/signup');
    });
});

3. 重置mock状态,避免测试污染

如果继续使用全局mock,需要在每个测试用例前重置mock,防止之前的测试影响当前用例:

describe('Signin Component', () => {
  beforeEach(() => {
    mockNavigate.mockClear();
  });

  it('navigates to signup page', async () => {
    // 测试代码...
  });
});

4. 确保userEvent操作是异步的

旧版本的@testing-library/user-event中,点击操作是异步的,必须用await包裹:

await userEvent.click(screen.getByTestId('test-nav-btn'));

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 05:53:30