こんにちは、かつコーチです。
前回はmiddleware.tsを使って、ログイン必須ページをまとめて保護する実装をしました。
認証まわりのロジックは特に、後から誰かが変更を加えたときに気づかず壊してしまいやすい部分です。
今回はそうした事故を防ぐために、Next.jsアプリにテストを導入する方法を解説します。
Next.jsのテストとは?前提知識
2種類のテストを使い分ける
Next.jsのテストは、大きく2つのレイヤーに分けて考えると理解しやすくなります。
1つは単体テスト(関数やコンポーネント単位で、内部ロジックが正しいかを検証するテスト)で、もう1つはE2Eテスト(End to End、ブラウザを実際に操作してユーザー視点でアプリ全体が動くかを検証するテスト)です。
前者にはVitest、後者にはPlaywrightを使うのが2026年時点のNext.js公式ドキュメントでも推奨されている組み合わせです。
なぜJestではなくVitestなのか
以前はJestが単体テストの定番でしたが、Vitestは設定がシンプルで実行速度も速く、Next.jsのApp Routerとの相性も良いため、現在は多くのプロジェクトでVitestが採用されています。
とはいえJestが使えなくなったわけではなく、既存プロジェクトの移行コストとのバランスで選ぶ場面もあります。
新規プロジェクトであれば、迷わずVitestを選んで問題ありません。
基本の書き方・実装手順
手順1:Vitestのセットアップ
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
setupFiles: ["./vitest.setup.ts"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./"),
},
},
});
// vitest.setup.ts
import "@testing-library/jest-dom/vitest";
environment: "jsdom"を指定することで、Node.js上でもブラウザのDOM APIを模したテスト環境が使えるようになります。
手順2:Server Componentではない部分の単体テスト
// app/components/CartBadge.tsx
export function CartBadge({ itemCount }: { itemCount: number }) {
return (
<div>
{itemCount > 0 && <span data-testid="badge">{itemCount}件</span>}
</div>
);
}
// app/components/CartBadge.test.tsx
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { CartBadge } from "./CartBadge";
describe("CartBadge", () => {
it("件数が0のときはバッジを表示しない", () => {
render(<CartBadge itemCount={0} />);
expect(screen.queryByTestId("badge")).not.toBeInTheDocument();
});
it("件数が1以上のときはバッジを表示する", () => {
render(<CartBadge itemCount={3} />);
expect(screen.getByTestId("badge")).toHaveTextContent("3件");
});
});
renderでコンポーネントを仮想的に描画し、screen経由で表示内容を検証します。
Server Componentは非同期関数のため直接renderできないので、Vitestでの単体テストは基本的にClient Componentが対象になります。
手順3:Playwrightのセットアップ
npm init playwright@latest
対話式のセットアップで、TypeScript利用の有無やテストディレクトリ名を聞かれるので、デフォルトのまま進めれば問題ありません。
手順4:E2Eテストの実装
// e2e/login.spec.ts
import { test, expect } from "@playwright/test";
test("未ログインでダッシュボードにアクセスするとログイン画面にリダイレクトされる", async ({ page }) => {
await page.goto("/dashboard");
await expect(page).toHaveURL(/\/login/);
});
test("ログインするとダッシュボードが表示される", async ({ page }) => {
await page.goto("/login");
await page.getByRole("button", { name: "GitHubでログイン" }).click();
// OAuth連携部分はモック化して省略
await expect(page.getByText("ダッシュボード")).toBeVisible();
});
前回実装したmiddlewareのリダイレクト処理が正しく動いているかを、実際のブラウザ操作を通じて検証できます。
つまずきやすい設定・注意点
Playwrightはローカルサーバーの起動をテストランナーに任せることもできます。
// playwright.config.ts
export default {
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
};
CI環境では毎回サーバーを立ち上げ直したいので、reuseExistingServerを環境変数で切り替えておくと便利です。
よくあるつまずきポイント・エラー対処
実際にハマった「fetch is not defined」エラー
私が実際につまずいた経験を1つ共有します。
APIを呼び出すユーティリティ関数のテストを書いたところ、次のエラーが出てテストが全滅しました。
ReferenceError: fetch is not defined
❌ Before:テスト環境がNode.jsのバージョンやfetch実装を考慮していなかった
原因を調べると、当時使っていたNode.jsのバージョンが古く、グローバルなfetchが未実装のバージョンでした。
Vitestのenvironment: "jsdom"はDOMをエミュレートしてくれますが、fetch自体の有無はNode.jsのバージョンに依存します。
✅ After:Node.jsのバージョンを最新のLTSに上げる
# .nvmrcなどでバージョンを固定しておく
nvm install --lts
nvm use --lts
Node.js 18以降ではfetchが標準搭載されているため、バージョンを上げるだけで解決しました。
CI環境とローカル環境でNode.jsのバージョンがずれていないかも、あわせて確認しておくとよい教訓になりました。
まとめ
この記事のポイント
- Next.jsのテストは単体テスト(Vitest)とE2Eテスト(Playwright)の2レイヤーで考える
- VitestはApp Routerとの相性がよく、2026年時点で単体テストの標準的な選択肢
- Client ComponentはTesting Libraryの
renderで単体テストできるが、Server Componentは直接テストできない - PlaywrightはmiddlewareのリダイレクトのようなE2Eの挙動を実ブラウザで検証できる
fetch未対応の古いNode.jsバージョンがテストエラーの原因になることがあるので、バージョン管理に注意する
次に読むべき記事
テストで安全にコードを変更できる基盤が整ったら、次はいよいよアプリを世界に公開する番です。
次回は、Next.jsアプリをVercelにデプロイする方法を解説します。
→ 次の記事:Next.jsアプリをVercelにデプロイする方法