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

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

问题分析与解决方案

核心问题

  1. Next.js Link组件的测试行为:测试环境中next/link不会触发真实路由跳转,也不会直接调用router.push,内部导航逻辑导致mock的push方法无法被触发。
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 01:03:08