課題
例えばこのページにあるような例をテストしたいとする。
import { useRouter } from 'next/router'; import Layout from '../components/MyLayout'; const Page = () => { const router = useRouter(); return ( <Layout> <h1>{router.query.title}</h1> <p>This is the blog post content.</p> </Layout> ); }; export default Page;
この時、useRouterで取得できるrouterはNext.jsが裏でいい感じに作成しているため、enzymeとかを使ってPageのユニットテストをしようとしても、routerがundefinedでqueryが取れず死んでしまう。
解決策
テストのときだけrouterをモックしてしまうと良い。ユーティリティとして以下のようなものを用意する。
test/element.ts
import React from 'react'; import { NextRouter } from 'next/router'; import { RouterContext } from 'next/dist/next-server/lib/router-context'; // NextのrouterをmockしたReactElementを返す。 // 例えば以下のようにすればSampleComponentに渡るrouter.queryをモック出来る // withMockedRouter( // {query: { hoge: "fuga", foo: "bar" }}, // <SampleComponent />, // ); export function withMockedRouter(router: Partial<NextRouter> = {}, children: React.ReactElement): React.ReactElement { const mockedRouter = { route: '', pathname: '', query: {}, asPath: '', push: async () => true, replace: async () => true, reload: () => null, back: () => null, prefetch: async () => undefined, beforePopState: () => null, events: { on: () => null, off: () => null, emit: () => null, }, ...router, }; return <RouterContext.Provider value={mockedRouter}>{children}</RouterContext.Provider>; }
これを使えば先程のテストは以下のように書ける(jestとenzymeを利用)。
pages/post.test.ts
import React from 'react'; import { withMockedRouter } from '../test/element'; import { mount } from 'enzyme'; import Page from './post'; describe('Post Page', () => { it('正しく表示できる', () => { const wrapper = mount( withMockedRouter( { query: { title: 'ページタイトル', }, }, <Page /> ) ); expect(wrapper.find('h1').text()).toBe('ページタイトル'); }); });
これでrouterの情報を使うページもユニットテスト出来るようになった。
補足
Next.jsがどのようにrouterを作っているかは、Next.jsのページ遷移・ページロードの仕組みをコードリーディングで追いかけた - $shibayu36->blog;で調査したので、興味があればこちらをどうぞ。