Router
などの機能をモックした render
を作成して、共通のコンポーネントとして利用することを目標としましょう。// src/pages/sample.tsx import { useRouter } from 'next/router'; export default function Sample(): JSX.Element { const router = useRouter(); const path = router.asPath; return <h1>このページの path は {path} です。</h1>; }
pages/...
となっているのは、以前の記事(【Next.js】特定のディレクトリを基準にし、絶対パスでモジュールをインポートする方法)で紹介した設定を行っているためです(推奨)// __test__/sample.test.tsx import { render } from '@testing-library/react'; import Sample from 'pages/sample'; describe('初期表示の確認', () => { test('サンプルページ', () => { const { asFragment } = render(<Sample />); expect(asFragment()).toMatchSnapshot(); }); });
# テストの実行 yarn test __test__/sample.test.tsx
# 表示されるエラーを抜粋 初期表示の確認 ✕ サンプルページ (29 ms) ● 初期表示の確認 › サンプルページ TypeError: Cannot read property 'asPath' of null 3 | export default function Sample(): JSX.Element { 4 | const router = useRouter(); > 5 | const path = router.asPath; | ^ 6 | 7 | return <h1>このページの path は {path} です。</h1>; 8 | } ... ```
router.asPath
のところでエラーが発生していることがわかります。next/router
を使用する際には必ず遭遇するエラーと言っても過言ではありません。useRouter
などの Hooks はクライアントサイド側で実行されるため、テスト実行時などサーバーサイド側でのみ実行する場合にエラーが起きてしまうのです。useRouter
意外にも use***
系の Hooks を使用する際には常に一工夫が必要です。モックオブジェクト(Mock Object)とは、ソフトウェアテスト時、特にテスト駆動開発、ビヘイビア駆動開発における代用の下位モジュールスタブの一種。スタブと比較して、検査対象のモジュールがその下位モジュールを正しく利用しているかどうかを検証するのに使われる。引用先
// __test__/sample.test.tsx import { render } from '@testing-library/react'; import Sample from 'pages/sample'; // モック用に追記 jest.mock('next/router', () => ({ useRouter() { return { asPath: '/', }; }, })); describe('初期表示の確認', () => { test('サンプルページ', () => { const { asFragment } = render(<Sample />); expect(asFragment()).toMatchSnapshot(); }); });
# テストの実行 yarn test __test__/sample.test.tsx PASS __test__/sample.test.tsx 初期表示の確認 ✓ サンプルページ (11 ms) › 1 snapshot written. ...
jest.mock()
の中にモックしたい機能を選択して、その置き換える機能と一緒に定義します。next/router
の中の useRouter
の機能の中にある asPath
を '/'
に置き換えるという処理を書いていました。userRouter
の asPath
だけで良かったのですが、useRouter
を使っていると他にも必要な機能をモックしていく必要があります。useRouter
の引数として取ることができる変数を next/router
の型として定義されている NextRouter
を参考に、以下のように設定していきました。// __test__/sample.test.tsx import { render } from '@testing-library/react'; import Sample from 'pages/sample'; // 変更 jest.mock('next/router', () => ({ useRouter() { return { route: '/', pathname: '/', query: {}, asPath: '/', basePath: '/', isLocaleDomain: true, isReady: true, push: jest.fn(), prefetch: jest.fn(), replace: jest.fn(), reload: jest.fn(), back: jest.fn(), beforePopState: jest.fn(), events: { on: jest.fn(), off: jest.fn(), emit: jest.fn(), }, isFallback: false, isPreview: false, }; }, })); describe('初期表示の確認', () => { test('サンプルページ', () => { const { asFragment } = render(<Sample />); expect(asFragment()).toMatchSnapshot(); }); });
# テストの実行 yarn test __test__/sample.test.tsx PASS __test__/sample.test.tsx 初期表示の確認 ✓ サンプルページ (12 ms) › 1 snapshot written. ...
render
コンポーネントをカスタムして、モック機能を持った render
コンポーネントを作成してしまうことです。render
を呼び出してもモック機能が付随しているため、モックがされていないといった小さなストレスのかかるエラーを回避できます。render
を作成するだけです。// __test__/utils.ts import Queries from '@testing-library/dom/types/queries'; import { render as defaultRender, RenderResult } from '@testing-library/react'; // import { RouterContext } from 'next/dist/next-server/lib/router-context'; import { RouterContext } from 'next/dist/shared/lib/router-context'; import { NextRouter } from 'next/router'; import React from 'react'; export function render(children: React.ReactElement): RenderResult<typeof Queries, HTMLElement> { const mockRouter: NextRouter = { route: '/', pathname: '/', query: {}, asPath: '/', basePath: '/', isLocaleDomain: true, isReady: true, push: jest.fn(), prefetch: jest.fn(), replace: jest.fn(), reload: jest.fn(), back: jest.fn(), beforePopState: jest.fn(), events: { on: jest.fn(), off: jest.fn(), emit: jest.fn(), }, isFallback: false, isPreview: false, }; return defaultRender( <RouterContext.Provider value={mockRouter}>{children}</RouterContext.Provider>, ); }
render
を defaultRender
として使用し、render
を再度定義しています。RenderResult
や NextRouter
などは TypeScript に対応させるために型の情報を見ながら調整していきました。RouterContext
というコンポーネントにモックに必要な情報を渡しているだけです。render
を利用すると、以下のようにテストが非常にスッキリとします。// __test__/sample.test.tsx // React Testing Library の render はコメントアウト // import { render } from '@testing-library/react'; import Sample from 'pages/sample'; // 新たに定義したカスタマイズ render を使用 import { render } from './lib/utils'; describe('初期表示の確認', () => { test('サンプルページ', () => { const { asFragment } = render(<Sample />); expect(asFragment()).toMatchSnapshot(); }); });
# テストの実行 yarn test __test__/sample.test.tsx PASS __test__/sample.test.tsx 初期表示の確認 ✓ サンプルページ (12 ms) › 1 snapshot written. ...
lib/utils.ts
でテストを効率よくかけるための処理をまとめていくと良いでしょう。render
を作るという話をほとんど見かけないため、今回は Blitz.js の知見をお借りしながら Next.js 用にまとめました。renderHooks
も準備していたため、また Hooks の処理が増えてきた頃にこの辺りも整理していきたいと思います。新着記事
関連記事
著者