Next.js 13 i18n测试问题:点击语言按钮未触发useRouter调用
问题:Next.js 13多语言切换按钮测试无法触发useRouter方法调用
我正在测试Next.js 13的多语言(i18n)切换功能,点击语言按钮后,无法验证对应的useRouter方法是否被调用。尝试过通过window.location获取路径变化、监听useRouter方法调用等方案,目前使用mock useRouter的方式有一定效果,但仍无法验证点击后的路由调用行为。
相关代码
组件:LangButtons.tsx
import useTranslations from '@/hooks/useTranslations' import Link from 'next/link' import { useRouter } from 'next/router' const LangButtons = () => { const { asPath } = useRouter() const { currentLang, langs } = useTranslations() return( <nav> {langs!.map( (lang) => currentLang !== lang && ( <Link key={lang} locale={lang} href={asPath}> <div role="button">{lang.split('-')[0]}</div> </Link> ), )} </nav> ) } export default LangButtons
测试文件:LangButtons.test.tsx
import { fireEvent, render, screen } from '@testing-library/react' import '@testing-library/jest-dom' import LangButtons from '.' const mockRouter = jest.fn() jest.mock('next/router', () => ({ ...jest.requireActual('next/router'), useRouter: () => mockRouter(), })) jest.mock('@/hooks/useTranslations', () => () => ({ currentLang: 'en-AR', langs: ['en-US', 'es-AR', 'pt-BR'], })) describe('LangButtons', () => { it('should change path on button click', () => { const onMock = jest.fn(() => Promise.resolve(true)) const emitMock = jest.fn(() => Promise.resolve(true)) const pushMock = jest.fn(() => Promise.resolve(true)) mockRouter.mockImplementation(() => ({ push: pushMock, prefetch: () => Promise.resolve(true), asPath: 'pt-BR', events: { off: jest.fn(), emit: emitMock, on: onMock, }, })) render(<LangButtons />) const portugueseButton = screen.getByRole('button', { name: /pt/i }) fireEvent.click(portugueseButton) expect(pushMock).toHaveBeenCalled() expect(emitMock).toHaveBeenCalled() expect(onMock).toHaveBeenCalled() }) })
依赖Hook:useTranslations.ts
import { useRouter } from 'next/router' import languages, { TranslationFile } from '@/locales/resources' const useTranslations = () => { const { locale, locales } = useRouter() const translations = languages[locale!] as TranslationFile return { translations, currentLang: locale, langs: locales } } export default useTranslations
资源文件:resources.ts
import EN from './translation-en.json' import ES from './translation-es.json' import PT from './translation-pt.json' export type TranslationFile = typeof import('./translation-en.json') interface Languages { [key: string]: TranslationFile } const languages: Languages = { 'en-US': EN, 'es-AR': ES, 'pt-BR': PT, } export default languages
问题分析与解决方案
核心问题
- Next.js Link组件的测试行为:测试环境中
next/link不会触发真实路由跳转,也不会直接调用router.push,内部导航逻辑导致mock的push方法无法被触发。 - Mock初始化顺序问题:原测试中
mockRouter变量在jest.mock之后定义,存在变量提升导致的作用域问题,组件渲染时获取的router实例并非预期的mock版本。
修复方案
方案1:调整mock方式,直接模拟useRouter返回值
import { fireEvent, render, screen } from '@testing-library/react' import '@testing-library/jest-dom' import LangButtons from '.' import { useRouter } from 'next/router' // 直接mock useRouter为jest函数 jest.mock('next/router', () => ({ useRouter: jest.fn(), })) jest.mock('@/hooks/useTranslations', () => () => ({ currentLang: 'en-AR', langs: ['en-US', 'es-AR', 'pt-BR'], })) describe('LangButtons', () => { it('should trigger router push with correct locale on button click', () => { const pushMock = jest.fn(() => Promise.resolve(true)) const testPath = '/demo-page' // 让useRouter返回预设的mock对象 ;(useRouter as jest.Mock).mockReturnValue({ push: pushMock, asPath: testPath, locale: 'en-AR', locales: ['en-US', 'es-AR', 'pt-BR'], }) render(<LangButtons />) const portugueseButton = screen.getByRole('button', { name: /pt/i }) fireEvent.click(portugueseButton) // 验证push方法的调用参数是否符合预期 expect(pushMock).toHaveBeenCalledWith( { pathname: testPath, query: {} }, testPath, { locale: 'pt-BR' } ) }) })
方案2:使用专门的路由mock库
安装next-router-mock来更精准模拟Next.js路由行为:
npm install --save-dev next-router-mock
修改测试文件:
import { fireEvent, render, screen } from '@testing-library/react' import '@testing-library/jest-dom' import LangButtons from '.' import { useRouter } from 'next/router' import mockRouter from 'next-router-mock' jest.mock('next/router', () => ({ ...jest.requireActual('next/router'), useRouter: () => mockRouter, })) jest.mock('@/hooks/useTranslations', () => () => ({ currentLang: 'en-AR', langs: ['en-US', 'es-AR', 'pt-BR'], })) describe('LangButtons', () => { beforeEach(() => { // 重置mock状态 mockRouter.push.mockClear() mockRouter.asPath = '/demo-page' mockRouter.locale = 'en-AR' }) it('should change locale on button click', () => { render(<LangButtons />) const portugueseButton = screen.getByRole('button', { name: /pt/i }) fireEvent.click(portugueseButton) expect(mockRouter.push).toHaveBeenCalledWith( '/demo-page', undefined, { locale: 'pt-BR' } ) expect(mockRouter.locale).toBe('pt-BR') }) })
方案3:修复原mock的初始化顺序
避免变量提升导致的作用域问题,直接在mock函数内部定义所需方法:
import { fireEvent, render, screen } from '@testing-library/react' import '@testing-library/jest-dom' import LangButtons from '.' // 直接在mock中定义router方法,避免外部变量引用问题 jest.mock('next/router', () => { const mockPush = jest.fn(() => Promise.resolve(true)) return { ...jest.requireActual('next/router'), useRouter: jest.fn(() => ({ push: mockPush, prefetch: () => Promise.resolve(true), asPath: 'pt-BR', events: { off: jest.fn(), emit: jest.fn(() => Promise.resolve(true)), on: jest.fn(() => Promise.resolve(true)), }, })), } }) jest.mock('@/hooks/useTranslations', () => () => ({ currentLang: 'en-AR', langs: ['en-US', 'es-AR', 'pt-BR'], })) describe('LangButtons', () => { it('should change path on button click', () => { const { useRouter } = require('next/router') const router = useRouter() render(<LangButtons />) const portugueseButton = screen.getByRole('button', { name: /pt/i }) fireEvent.click(portugueseButton) expect(router.push).toHaveBeenCalled() }) })
额外注意点
- 原
resources.ts中有笔误:'es-AR': IS,应改为'es-AR': ES,,需修正避免运行时错误。 - 若项目使用Next.js 13 App Router,应使用
next/navigation中的useRouter,而非next/router,需确认路由模式是否匹配。
内容的提问来源于stack exchange,提问作者Lissette
相关产品推荐
相关产品推荐

