https://zenn.dev/takepepe/articles/nextjs13-components-integration-test

Next.js 13 新機能の App ディレクトリの勉強がてら、結合テストをどう書いていったらよいか考察しました。参照している公式ドキュメントが beta 版なのはもちろん、App ディレクトリそのものが beta 版なので、実運用には使えるものではないと思うので予めご了承ください。一応こんな感じで書けそう、という話です。

結合テストの準備

本稿が指す結合テストとは、App ディレクトリのルートセグメントを構成する、特別なファイル(Special Files)が、与えた状態に応じてどのように表示されるかを検証するテストを指します。

ルートが外部から受ける要因として大きいものが、URL に含まれるクエリパラメーター・パスパラメーターです。コンポーネントは、これらの値を参照して API サーバーにリクエストしたり、ORM ライブラリからクエリーを発行したりなど、コンポーネント表示に必要な処理を実行します。本稿で紹介する結合テストは、その一連の入力から出力までの範囲が観点です。

冒頭に貼ったリポジトリにコミットしたapp/testのルートを構成するファイル群の結合テストを見ていきます。まず次のように、ディレクトリに設置しているファイルを import し、filesオブジェクトにまとめます。

このファイル群をrenderRoute関数を実行してテスト時にレンダーする、というのが大筋の概要です。beta 版ドキュメントに書かれている内容から筆者が書き起こした、テスト向けの再現関数です。

引数内訳は次のとおりで、ルート URL を参照する値(searchParams, params)はテスト毎に手動で与えるものとします。

構成ファイルに含まれる、要素表示のテスト

次のように構成ファイル群を使用しrenderRouteを実行します。はじめに、Layout コンポーネントの要素テスト対象要素(1)と、Page コンポーネントに含まれる要素テスト対象要素(2)がレンダーされていることを確認します。

test("When data fetch succeed, All contents will be display", async () => {
  await renderRoute({ ...files });
  // Layout に含まれる要素 / テスト対象要素(1)
  expect(screen.getByRole("heading", { name: /Test/i })).toBeInTheDocument();
  // Page に含まれる要素 / テスト対象要素(2)
  expect(screen.getByRole("link", { name: "down" })).toBeInTheDocument();
});

export default function Layout({ children }: { children: ReactNode }) {
  return (
    <div className={styles.module}>
      <h2>Test</h2> {/* テスト対象要素(1) */}
      {children}
      <Link href={`/`}>/</Link>
    </div>
  );
}

export default withZod(
  { searchParams: { greet: z.string().optional() } },
  async ({ searchParams: { greet } }) => {
    const { message } = await getMessage({ greet });
    if (!message) throw notFound();
    return (
      <div className={styles.module}>
        <p>{message}</p>
        {greet && <p>{greet}</p>} {/* ?greet= があれば表示 */}
        <Link href="/test/1">down</Link> {/* テスト対象要素(2) */}
      </div>
    );
  }
);

動的パラメーターによる、表示分岐のテスト

次に、Query パラメーターが反映されることを検証するテストを書きます。Query パラメーターは Page コンポーネントの props searchParamsで参照できるので、?greet=Hiというリクエストを次のテストで再現します。

test("When access with ?greet=, value will be display", async () => {
  const searchParams = { greet: "Hi" };
  await renderRoute({ ...files, searchParams });
  // query パラメータ ?greet= に値がある場合、表示される
  expect(screen.getByText(searchParams.greet)).toBeInTheDocument();
});