Next.js の getServerSideProps & API Routes テスト手法についてまとめました。getServerSidePorps & API Routes に関するテストは「Cypress・Playwright」を利用することが多いと思いますが、本稿は Jest 単体テストの紹介です。以下はテストに使用するエコシステム一式です(Jest 等は略)
本題に入る前に、pageExtensions をnext.config.js
に設定します。pageExtensions はpages
に含まれるファイルのうち、指定の拡張子をもつファイルが「Page・API 実装ファイルである」ことを指定するものです。
module.exports={pageExtensions:["page.tsx","api.ts"],};
この設定で、テストファイルをpages
に配備することが可能になります。実装ファイルのそばにテストファイルを置くことで、テストが書かれているかが一目瞭然になります。それぞれ同名の test.tsx
・test.ts
が対応するテストファイルです。
機能 | 実装ファイル | テストファイル |
---|---|---|
getServerSideProps | pages/example.page.tsx | pages/example.test.tsx |
API Routes | pages/api/example.api.ts | pages/api/example.test.ts |
モックにはMSW を利用します。ハンドラー関数は一度セットしてお終いではなく、テストケースによってレスポンスを変更したい事が多いです。そこで、以下の様な「ハンドラー関数を作るファクトリー関数」を用意しておくと、テストケースごとに API 詳細を意識する必要がなくなります。例えば以下のファクトリー関数は、200 | 400 のいずれかのレスポンスを返すハンドラーを作れます。
exportconstcreateHandler=(status:200|400=200)=> rest.post<Data,{ id:string}, Data| Err>(path(),(req, res, ctx)=>{if(status===400||!req.body.title)returnres( ctx.status(400), ctx.json({ message:"Bad Request", status:400}));returnres(ctx.json(req.body));});
ハンドラーをインターセプトする際はserver.use
を使用しますが、簡潔になることが見てとれます。ここでは単純なファクトリー関数としていますが、I/O はいくらでも工夫の余地があります。
test("400",async()=>{// Intercept mock Error server.use(createHandler(400));// some test case});
余談ですが、このハンドラーファクトリー関数をデータ取得関数(fetcher)とセットで定義しておくと、スコープを特定しやすく・資材をまとめやすくなります。
src/fetcher├── posts│ ├── create│ │ ├── index.ts│ │ └── mock.ts│ ├── delete│ │ ├── index.ts│ │ └── mock.ts│ ├── list│ │ ├── index.ts│ │ └── mock.ts│ ├── show│ │ ├── index.ts│ │ └── mock.ts│ └── update│ ├── index.ts│ └── mock.ts└── type.ts
mock.ts
にハンドラーファクトリー関数が含まれます。関連する型定義などを同包しても良いでしょう。aspida など、データ取得 client を生成している場合も同様です。
いよいよテストケースの作成です。プロジェクトで定義している MSW ハンドラーをセット、これがデフォルトの API モックとなります。
const server=setupMockServer(...handlers);
getServerSideProps
関数内部に実装されている一連のデータ取得は、デフォルトの API モックレスポンスを得た状態になります。その結果を、<Page />
コンポーネントに展開し、期待する正常系コンポーネント表示に至ったかを検証します。
test("If the data acquisition is successful, the title will be displayed.",async()=>{const res=awaitgetServerSideProps(gsspCtx());assertHasProps(res);render(<Page{...res.props}/>);expect(screen.getByText("Posts")).toBeInTheDocument();});
正常系のデフォルト API モックをserver.use
で異常系に上書きします。以下の例では、データ取得に失敗(500 エラー)するものとしています。期待する異常系コンポーネント表示に至ったかを検証します。
test("If data acquisition fails, an error will be displayed",async()=>{ server.use(postListHandler(500));// Intercept mock Errorconst res=awaitgetServerSideProps(gsspCtx());assertHasProps(res);render(<Page{...res.props}/>);expect(screen.getByText("Internal Server Error")).toBeInTheDocument();});
以下のようにgsspCtx({ query: { id: "lorem-ipsum" } })
とすることで、getServerSideProps
関数が参照する query param や path param を模すことができます。パラメーターに応じてコンポーネントを出し分けるページでは、パラメーター名称変更に伴うリグレッションを防げるでしょう。
test("If the data acquisition is successful, the title will be displayed.",async()=>{const res=awaitgetServerSideProps(gsspCtx({ query:{ id:"lorem-ipsum"}}));assertHasProps(res);render(<Page{...res.props}/>);expect(screen.getByText("Post: Lorem ipsum")).toBeInTheDocument();});
先の例で使用していたgsspCtx()
関数は、getServerSideProps
関数の引数ctx
作成関数です。node-mocks-httpのcreateRequest
とcreateResponse
を適用しつつ、query など値を注入できるようにしていました。
exportconst gsspCtx=( ctx?: Partial<GetServerSidePropsContext>): GetServerSidePropsContext=>({ req:createRequest(), res:createResponse(), params:undefined, query:{}, resolvedUrl:"",...ctx,});
res をアサートしてるのはgetServerSideProps
関数戻り値にprops
が含まれないことがあるからです。Assertion Functions として定義したassertHasProps
関数を通過すると「props が存在する」と判定されます。
classAssertionErrorextendsError{}exportfunctionassertHasProps<T>( res: GetServerSidePropsResult<T>):asserts resis{ props:T}{const hasProps=typeof res==="object"&&(resasany)["props"]&&typeof(resasany).props==="object";if(!hasProps)thrownewAssertionError("no props");}
API Routes のテストはnext-test-api-route-handler を使うと簡単に行えます。testApiHandler
関数のパラメータに、API Routes 定義であるhandler
を渡します。(MSW のハンドラーではありません)特に凝ったことはしていないので、詳細は next-test-api-route-handler の README をご参考ください。
test("201",async()=>{awaittestApiHandler({ handler, url:"/api/posts",test:async({ fetch})=>{const res=awaitfetch(requestInit);awaitexpect(res.json()).resolves.toStrictEqual(body);},});});
MSW ハンドラーをインターセプトする要領はgetServerSideProps
関数と同じです。server.use(postCreateHandler(400));
で 400 レスポンスを再現します。
test("400",async()=>{// Intercept mock Error server.use(postCreateHandler(400));awaittestApiHandler({ handler, url:"/api/posts",test:async({ fetch})=>{const res=awaitfetch(requestInit);awaitexpect(res.json()).resolves.toStrictEqual({ message:"Bad Request",});},});});
Cypress・Playwright のテストケース全てを代替するとまではいきませんが、多くのケースがカバー出来るのではないでしょうか。本稿サンプルコードは以下に公開しています。