Next 14のバージョンアップ時のテストのモックの仕方

背景

Next.jsのバージョンを12から14にアップデートする際に、これまでルーターのテスト(jest)に使っていたnext-router-mockが動かなくなってしまいました。 確かに、Next.js13まで動作可能だと、ドキュメントにはきちんと記載してあります。 問題が新しすぎるのか、常識過ぎるのせいかわかりませんが、記事や公式ドキュメントには解決策がまとまっていなかったので、 Next.js14のルーターテストを含めてテストの仕方をまとめておきます。

公式の設定

公式には以下のように書かれていますが、このあたりをやっておきましょう。 他に、 必要なことがあればNext.jsの公式を確認して対応してください。

ライブラリのインストール

yarn add -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom

Jestの設定ファイルの作成

import nextJest from 'next/jest.js'

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
})

// Add any custom config to be passed to Jest
const config: Config = {
  coverageProvider: 'v8',
  testEnvironment: 'jsdom',
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config)

カスタムモックの実装

Next.jsのルーターをモックしてテストを書いた方法です。 jest.setup.tsまたはjest.setup.jsファイルで、Next.jsのルーターを以下のようにモック化します。

// nextjsのrouterをモック化する
let pathname = ''
let query = {}

const pushMock = jest.fn((args) => {
  if (typeof args === 'string') {
    pathname = args
  }
  if (typeof args === 'object') {
    if (args?.pathname) {
      pathname = args.pathname
    }
    if (args?.query) {
      query = args.query
    }
  }
})

jest.mock('next/router', () => ({
  useRouter() {
    return {
      push: pushMock,
      goBack: jest.fn(),
      get query() {
        return query
      },
      set query(value) {
        query = value
      },
      route: '/',
      get pathname() {
        return pathname
      },
      set pathname(value) {
        pathname = value
      },
      asPath: '',
    }
  },
  push: pushMock,
  goBack: jest.fn(),
  route: '/',
  get pathname() {
    return pathname
  },
  set pathname(value) {
    pathname = value
  },
  get query() {
    return query
  },
  set query(value) {
    query = value
  },
  asPath: '',
}))

beforeEach(() => {
  jest.clearAllMocks()
  pathname = ''
  query = {}
})

このモックでは、useRouterフックをモック化し、push、goBack、query、pathnameなどの基本的なルーター機能を再現しています。 テスト時の遷移をテストするために、pushMock関数を使って、別で宣言したpathnameとquery変数を変更しています。getterとsetterを使うことで、テスト中にこれらの値を動的に変更されたものを参照できるようにしています。 beforeEachで、忘れずにリセットしておくことも重要です

実際のテスト

実際のテストでこのモックを使って、ルーターの遷移やクエリパラメータの変更をテストします。

URLのテスト

import router from 'next/router'

  it('プロフィール登録済みカスタマーが、プロフィール新規登録画面がアクセスしたときに、top画面に遷移すること', async () => {
    // ログイン済みのみ、プロフィールのリクエストが飛ぶ
    await router.push('/mypage/profile/new')
    render(
      <MockedProvider mocks={mocks}>
        <FirstRegistrationRedirect />
      </MockedProvider>,
    )

    await waitFor(() => expect(router.pathname).toBe('/'))
  })

FirstRegistrationRedirectコンポーネントの内部では、grapqlのレスポンス(MockedProviderで与えています)と現在のURLをみて、遷移させるか決めています。コンポーネント内部ではmockされたrouterを参照して、遷移を行います。 テスト側は、pathnameが変わっていれば、リダイレクトされていることがわかります。レンダリングされてから、pushが呼び出されるまで何度か再レンダリングが必要で、そのた待機のためにwaitForを使っています。

クエリパラメータのテスト

 it('プロフィール未登録カスタマーが、プロフィール新規登録画面郡以外にアクセスしたときに、queryにreturnUrlを渡して、プロフィール新規登録画面に遷移すること', async () => {
    router.pathname = '/mypage'

    render(
      <MockedProvider mocks={mocks}>
        <FirstRegistrationRedirect />
      </MockedProvider>,
    )
    await waitFor(() =>  expect(router.push).toHaveBeenCalledWith({
      pathname: '/mypage/profile/new',
      query: { returnUrl: '/mypage' },
    }))
   
  })

ここでは、router.queryを参照してテストをする事もできますが、router.pushを呼び出す際に、引数が正しいかどうかを確認しています。 このようにルーターの遷移やクエリパラメータの変更をテストもできます。

まとめ

Next.js 14へのアップデートによるjestテストの問題に対処するために、独自のルーターモックを実装しました。この方法により、ルーターの遷移やクエリパラメータの変更をテストすることができます。同様の問題に直面している方の参考になれば幸いです。