从Jest迁移到Vitest后,useNavigate的mockNavigate未被调用问题
问题场景
我正从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
相关产品推荐
相关产品推荐

