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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 15:24:53