Next.js组件Vitest测试中useSearchParams返回null/undefined如何解决?
问题:Next.js + Vitest 保留原生
useSearchParams 时返回 null 我正在开发Next.js项目并使用Vitest做测试,需要mock next/navigation 模块中的useRouter和usePathname,但保留useSearchParams的原生实现。但组件中打印useSearchParams的返回值始终是null,尝试mockRestore和importActual都无效。
组件简化版本
'use client' import { useState } from "react" import { usePathname, useRouter, useSearchParams } from "next/navigation" import { useDispatch } from "react-redux" const LoginStep = () => { const dispatch = useDispatch() const router = useRouter() const sp = useSearchParams() console.log(sp) // 此处打印null const [isLoading, setIsLoading] = useState(false) const [email, setEmail] = useState("") const [password, setPassword] = useState("") const login = async (e) => { e.preventDefault() try { setIsLoading(true) // 登录逻辑 router.replace(sp.get("origin") || "/workspace/trading/bot?page=1&filter=all") } catch (error) { setIsLoading(false) } } return ( <form onSubmit={login}> <input value={email} onChange={(e) => setEmail(e.target.value)} /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> <button type="submit" disabled={isLoading}>Login</button> </form> ) } export default LoginStep
当前测试配置
我为next/navigation创建了手动mock,试图保留useSearchParams的原生功能:
手动mock文件 __mocks__/next/navigation.ts
import { useSearchParams } from 'next/navigation'; import { vi } from 'vitest'; const mockedUseRouterReturnValue = { push: vi.fn(), back: vi.fn(), forward: vi.fn(), prefetch: vi.fn(), refresh: vi.fn(), replace: vi.fn() } const mockedUseRouter = vi.fn(() => mockedUseRouterReturnValue); const mockedUsePathname = vi.fn(); export { mockedUseRouter as useRouter, mockedUsePathname as usePathname, useSearchParams }; export { mockedUseRouterReturnValue }
测试文件
import { fireEvent, screen, } from "@testing-library/react" import { renderWithProviders } from "@/utils/testing-utils/providers" import LoginStep from "." import { mockLoginToServiceAction, mockedLoginToService } from "@/__mocks__/actions/auth/login" import { useSearchParams } from "next/navigation" import { vi, describe, it, expect, beforeEach } from "vitest" vi.mock('next/navigation') describe('Given the user has entered all fields correctly on /login?origin=/account/exchanges/binance', it('When they click "Login" button', async () => { vi.mocked(useSearchParams).mockRestore() const { container } = renderWithProviders( <LoginStep setLoginStep={(value) => {}} /> ) }) })
解决方案
问题根源
在手动mock文件中直接导入useSearchParams是无效的——Vitest会优先加载mock目录下的文件,导致你导入的是mock文件自身的useSearchParams,而非真正的next/navigation原生实现。
修复步骤
1. 修改手动mock文件
使用vi.importActual获取原生模块,确保导出的useSearchParams是原生实现:
// __mocks__/next/navigation.ts import { vi } from 'vitest'; // 导入原生next/navigation模块 const actual = await vi.importActual('next/navigation'); const mockedUseRouterReturnValue = { push: vi.fn(), back: vi.fn(), forward: vi.fn(), prefetch: vi.fn(), refresh: vi.fn(), replace: vi.fn() } const mockedUseRouter = vi.fn(() => mockedUseRouterReturnValue); const mockedUsePathname = vi.fn(); export { mockedUseRouter as useRouter, mockedUsePathname as usePathname, // 导出原生useSearchParams actual.useSearchParams as useSearchParams }; export { mockedUseRouterReturnValue }
2. 测试文件中注入搜索参数
原生useSearchParams依赖Next.js路由上下文,测试时需手动模拟参数。可以直接mockuseSearchParams返回预期的URLSearchParams实例:
import { fireEvent, screen, } from "@testing-library/react" import { renderWithProviders } from "@/utils/testing-utils/providers" import LoginStep from "." import { useSearchParams } from "next/navigation" import { vi, describe, it, expect } from "vitest" // 覆盖mock,自定义useSearchParams返回值 vi.mock('next/navigation', async () => { const mocked = await vi.importMock('next/navigation'); return { ...mocked, useSearchParams: () => new URLSearchParams('origin=/account/exchanges/binance') } }) describe('Given the user has entered all fields correctly on /login?origin=/account/exchanges/binance', () => { it('When they click "Login" button', async () => { const { container } = renderWithProviders( <LoginStep setLoginStep={(value) => {}} /> ) // 触发登录并断言逻辑 fireEvent.submit(screen.getByRole('form')); const { mockedUseRouterReturnValue } = await import('next/navigation'); expect(mockedUseRouterReturnValue.replace).toHaveBeenCalledWith('/account/exchanges/binance'); }) })
3. 关键注意事项
- 不要在测试中调用
vi.mocked(useSearchParams).mockRestore(),这会将其重置为Vitest默认的空mock(返回null)。 - 如果你的
renderWithProviders未包含Next.js路由上下文,要么在其中添加上下文提供者,要么直接mockuseSearchParams返回自定义实例。
内容的提问来源于stack exchange,提问作者ArtemBohak
相关产品推荐
相关产品推荐

