Next.js App Router 時代のテスト戦略:Jest・RTL・Playwrightで支える開発の安心感
はじめに
なぜ今「Next.js App Router + Testing」の構成が重要なのか
Next.js 13 以降で登場した「App Router」は、それまでの pages ディレクトリベースのルーティングと比べて、ディレクトリ構造や設計思想に大きな変化をもたらしました。layout.tsx
や template.tsx
、server components
の導入によって、アプリケーションの構築に柔軟性とパフォーマンスがもたらされた一方で、「どうやってテストを書くべきか?」という悩みが一気に増えたという声も多く耳にします。
特に、
next/navigation
のHookを使っているコンポーネントのモック方法app/
ディレクトリ構造特有のコンポーネント分離とそのテスト単位の考え方server component
とclient component
の混在によるテスト戦略の見直し
といった点で、「これまでのやり方が通用しない」と感じている人は少なくないはずです。
本記事の対象読者
このブログ記事は、以下のような方に向けて書いています。
- Next.jsのApp Routerで開発しているが、テストの構成に悩んでいる方
- React Testing Library や Jest の導入まではできたけど、運用に不安がある方
- 統合テストやモックのベストプラクティスを模索している方
- 今後のテスト戦略をチームで整備していきたい方
本記事では、Next.js App Router環境におけるユニットテスト/統合テストの構成方法や、ハマりやすいポイント、モックのテクニックを実践ベースで紹介していきます。
とりあえずこの構成をベースにすれば、ひとまず安心という状態を目指して、一歩ずつ丁寧に整理していきますので、どうぞ最後までお付き合いください。
今回のゴール
このブログ記事では、Next.js App Router構成のプロジェクトにおいて、
- ユニットテスト(Jest × React Testing Library)
- 統合テスト(ページ単位での動作確認)
- E2Eテスト(Playwrightによる実際のブラウザ操作)
の3つのレイヤーに分けて、テストの構成方法と実装例を紹介します。
特に、App Router固有の構成(layout.tsx, template.tsx, Server Actions など)に対応するための具体的な書き方やモックの方法を通じて、「テストを書きたくても最初の一歩が踏み出せない」状態から抜け出すことを目指します。
この記事を読み終えるころには、あなたのNext.jsアプリにテストを組み込み、安心して開発・リファクタリングできる構成を自力で作れる状態になっているはずです。
今回実装したコードはこちらのリポジトリに格納してありますので合わせてご参照ください。
基本構成と前提
Next.js App Router と Jest / React Testing Library を組み合わせてテストを構成する際、まず押さえておきたいのが 「どこまでをテストするのか」と「どの単位で分けるのか」という設計視点です。
App Routerはページ単位のコンポーネント分割(layout.tsx
, template.tsx
, page.tsx
)が特徴的な構成になっており、ユニットテストと統合テストを自然に分離しやすい構造とも言えます。
使用技術スタック
本記事では以下の構成で解説を進めていきます:
分類 | ライブラリ名 | 用途 |
---|---|---|
フレームワーク | Next.js(App Router) | ルーティング/レンダリング |
テスティング | Jest | テストランナー |
テスティング | React Testing Library | コンポーネントの振る舞いテスト |
補助 | @testing-library/jest-dom | DOMアサーションの拡張 |
補助(必要に応じて) | MSW / next-auth, react-query など | モックAPI/状態管理の補助 |
※ バンドラはSWCを使用します(Next.jsのデフォルト設定)
なぜ Jest + React Testing Library?
App Router構成においても、“振る舞いを中心にテストする” というReact Testing Libraryの思想は非常にマッチしています。
getByRole
,getByText
,getByLabelText
によるアクセシブルなセレクター- Jestの強力なmock機能との連携(
next/navigation
やnext-auth
のモックなど) - DOMの状態やコンポーネントの出力結果を検証しやすい
これにより、「UIとしてどう振る舞うべきか」を開発者目線で自然に確認できるのが最大の魅力です。
App Routerにおけるテストファイルの構成パターン
まず、下記を大前提としてテストファイルをどう配置するのが理想なのかを状況に応じてご紹介します。
components/
→ 再利用可能なUIパーツ(Client Componentが多い)app/
→ ページとレイアウト(Server Component含む)tests/
や__tests__/
→ 規模が大きくなったら分離管理してもOK
パターン①: Componentごとに隣接させる方式
components/
├── Button.tsx
├── Button.test.tsx ← ★同階層に配置
├── Header.tsx
├── Header.test.tsx
向いてるケース
- 小〜中規模のUIコンポーネント群
client component
中心で完結- Storybookと連携する場合にも扱いやすい
app/
配下は__tests__
などにまとめる方式
パターン②: app/
├── dashboard/
│ ├── page.tsx
│ └── __tests__/
│ └── page.test.tsx ← ★ページ単位でテスト
├── layout.tsx
└── __tests__/
└── layout.test.tsx ← ★共通layoutのテスト
向いてるケース
layout.tsx
,page.tsx
,template.tsx
などを明示的に分けている構成- ユーザーの遷移やページ描画結果など、統合的な振る舞いを確認したいとき
- モックの粒度を切り替えてテストしたいとき
パターン③: tests/pages や tests/components に全て集約する方式
tests/
├── components/
│ └── Button.test.tsx
├── pages/
│ └── dashboard.test.tsx
├── layout/
│ └── root-layout.test.tsx
向いてるケース
- コンポーネント単体 vs ページ全体をしっかり分離したいとき
- ディレクトリ内をスキャンしてテストを走らせたいCI構成
- ディレクトリ構成が複雑になる大規模アプリケーション
テストファイルの構成パターンまとめ
個人開発や小規模PJなら「① + ②の併用」が一番扱いやすくておすすめです。今回の記事も「① + ②の併用」で実装を進めていきます。
テスト対象 | 推奨パターン |
---|---|
再利用UI(Buttonなど) | パターン①(隣接) |
各ページの描画・振る舞い | パターン②(app配下に__tests__) |
CIや分業考慮で整理したい | パターン③(tests配下に集約) |
テスト環境のセットアップ(Next.js App Router + Jest + RTL)
App Router構成のNext.jsでJestとReact Testing Libraryを使うには、少しだけ注意点があります。
この章では、最小構成で動くテスト環境をセットアップする手順を丁寧に整理します。
Next.jsプロジェクトを作成します。
npx create-next-app@latest your-app-name
必要なパッケージのインストールをします。
npm install --save-dev jest @types/jest ts-jest ts-node \
@testing-library/react @testing-library/jest-dom jest-environment-jsdom \
@testing-library/user-event
jest
- テストランナー
- テストを実行するコアライブラリ
describe
,test
,expect
などの関数を提供- スナップショットテストやmock機能も豊富!
@types/jest
- JestのTypeScript型定義
- TypeScriptで
expect()
やjest.fn()
などを使うときに型が付くようになる
ts-jest
- TypeScriptファイルをJestで直接扱えるようにするトランスパイラ
- JestがTypeScriptコードを直接扱えるようになります。
@testing-library/react
- Reactの振る舞いテストに特化したライブラリ
render()
,screen.getByText()
,fireEvent()
などが使える- 「見た目」ではなく「どう振る舞うか」をテストする思想
@testing-library/jest-dom
toBeInTheDocument()
,toHaveClass()
などのカスタムマッチャーを追加- より表現豊かなアサーションが可能になる
- Jest + RTLではほぼ必須のセット
jest-environment-jsdom
- Jest v27までは jest 本体に
jsdom
がバンドルされていたが、v28以降で分離された - Jestをv28以降で使っていて
testEnvironment: 'jsdom'
を指定している場合は、明示的にjest-environment-jsdom
を追加する必要がある
@testing-library/user-event
- より現実的なユーザー操作(キーボード入力、マウス操作など)を再現できる
fireEvent()
よりも実際のユーザー行動に近い操作が可能- 例:
userEvent.click()
,userEvent.type(input, 'text')
など
- 例:
まとめ
パッケージ名 | 役割 |
---|---|
jest | テストランナー(テスト実行本体) |
@types/jest | Jestの型定義(TS用) |
ts-jest | TypeScript対応のためのJestトランスパイラ |
@testing-library/react | Reactの振る舞いテストライブラリ |
@testing-library/jest-dom | DOMアサーション用のマッチャー追加 |
@testing-library/user-event | より現実に近いユーザー操作のシミュレート |
jest.config.ts
)
jest の設定ファイル(import nextJest from 'next/jest'
const createJestConfig = nextJest({
dir: './',
})
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testEnvironment: 'jsdom',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1', // tsconfigのpaths対応
},
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
}
export default createJestConfig(customJestConfig)
import nextJest from 'next/jest'
- Next.js公式が提供しているJestの設定補助ユーティリティ。
- next/jest は、Next.jsに最適化された jest.config を生成するための関数を提供する。
const createJestConfig = nextJest({ dir: './' })
dir: './'
は、プロジェクトのルートディレクトリ(Next.jsのnext.config.js
やtsconfig.json
がある場所)を指定。- ここを明示することで、Next.jsの各種設定ファイルをjestが正しく読み込めるようにする
setupFilesAfterEnv
- 各テストの前に一度だけ読み込まれる設定ファイルを指定。
- よくある中身:@testing-library/jest-dom の読み込み、MSWの設定など。
testEnvironment
- テストを Node.js ではなくブラウザ環境(仮想DOM)で実行する設定。
- Reactコンポーネントなどのテストでは必須。
- Server Componentなどで DOM が一切不要なテストに関しては、個別に node を指定。
moduleNameMapper
@/components/Button
のようなパス指定を、Jestが解釈できるようにするための設定。tsconfig.json
の"paths"
に対応させたもの。
jest.setup.ts
)
Jest セットアップファイル(import '@testing-library/jest-dom'
これでtoBeInTheDocument()
など、便利なカスタムアサーションが使えるようになります。
tsconfigのtypes
Jestが @testing-library/jest-dom
の型を認識できていない場合は下記を設定します。
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
},
+ "types": ["@testing-library/jest-dom"],
},
scriptsに追加
{
"name": "next-app-testing-lab",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
+ "test": "jest",
"lint": "next lint"
},
動作確認用のテストコード作成
セットアップを行なったjest
等が正常に動作するか確認するためにコンポーネントの作成とそれのテストコードを作成します。
一般的なボタンコンポーネントを作成します。
'use client'
type Props = {
label: string
onClick: () => void
}
export const Button = ({ label, onClick }: Props) => {
return (
<button
type="button"
onClick={onClick}
className="rounded bg-blue-500 px-4 py-2 text-white"
>
{label}
</button>
)
}
上記に対応するテストコードとなります。
import { render, screen } from '@testing-library/react'
import { Button } from './Button'
test('renders button', () => {
render(<Button label="Hello" onClick={() => {}} />)
expect(screen.getByText('Hello')).toBeInTheDocument()
})
このテストではボタンコンポーネントが正しくレンダリングされるかを確認しています。
下記コマンドでテストを実行します。
npm run test
1件のテストが実行され正常終了しました。
これでNext.jsプロジェクトでクライアントコンポーネントのテストが実行できるようになりました。
ディレクトリ構成の確認
いくつかファイルを作成しましたので念のため現在のディレクトリ構成を記載しておきます。
tree -I node_modules -I .git -I public -I .swc --dirsfirst -a
.
├── app
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── Button.test.tsx
│ └── Button.tsx
├── .gitignore
├── README.md
├── eslint.config.mjs
├── jest.config.ts
├── jest.setup.ts
├── next-env.d.ts
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
└── tsconfig.json
ユニットテストの基本:Client Component編
Next.js の App Router を使ったプロジェクトでは、server component
と client component
の役割が明確に分かれています。
このセクションでは、"use client"
が付いたコンポーネント(= client component)に対して、React Testing Library と Jest を使ってユニットテストを書く基本的な方法を紹介します。
Client Component をテストするとは?
client component は、ボタンのクリックやフォームの入力といったブラウザ上のユーザー操作に関わる処理を担当します。そのため、テストでは以下のような観点を確認するのが基本となります。
- コンポーネントが正しくレンダリングされるか?
- イベント(クリックや入力)が正しく処理されるか?
- 状態に応じた表示切り替えが行われるか?
Button コンポーネント
テスト対象のコード
'use client'
type Props = {
label: string
onClick: () => void
}
export const Button = ({ label, onClick }: Props) => {
return (
<button
type="button"
onClick={onClick}
className="rounded bg-blue-500 px-4 py-2 text-white"
>
{label}
</button>
)
}
use client
を指定することで、onClick()
などのイベント処理が有効になります。
テストコード
import { render, screen } from '@testing-library/react'
import { Button } from './Button'
test('テスト①: 正しくラベルが表示されるか', () => {
render(<Button label="Click me" onClick={() => {}} />)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
test('テスト②: クリックされたときにハンドラが呼ばれるか', () => {
const handleClick = jest.fn()
render(<Button label="Click me" onClick={handleClick} />)
screen.getByRole('button').click()
expect(handleClick).toHaveBeenCalledTimes(1)
})
- テスト①: 正しくラベルが表示されるか
- このテストは、渡した label がボタンに表示されているかどうかを検証している。
screen.getByText('Click me')
は、画面上に "Click me" というテキストを持つ要素を取得。toBeInTheDocument()
は、実際にその要素がDOMに存在していることを確認するためのアサーション。
- テスト②: クリックされたときにハンドラが呼ばれるか
- コンポーネントがクリックイベントを正しく処理しているかどうかを検証している。
jest.fn()
は モック関数。呼び出し回数・引数などを確認できる。render()
でモック関数を渡した Button を描画。screen.getByRole('button')
で<button>
要素を取得し、click()
を実行。- 最後に
handleClick
が1回呼ばれたことをtoHaveBeenCalledTimes(1)
で検証。
Form コンポーネント
テスト対象のコード
'use client'
import { useState } from 'react'
export const SimpleForm = () => {
const [name, setName] = useState('')
const [submitted, setSubmitted] = useState(false)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (name.trim()) {
setSubmitted(true)
}
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">名前:</label>
<input
id="name"
name="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<button type="submit">送信</button>
{submitted && <p>こんにちは、{name}さん!</p>}
</form>
)
}
useState
で「入力された名前」「フォームが送信されたかどうかの状態」を保持e.preventDefault()
によって、ブラウザ標準のページリロードを抑止- 空欄で送信されないように
name.trim()
をチェック(簡易バリデーション) - 正常に入力があれば
submitted
をtrue
にして表示を切り替える
テストコード
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { SimpleForm } from './SimpleForm'
describe('SimpleForm', () => {
it('テスト①: 名前を入力して送信するとメッセージが表示される', async () => {
render(<SimpleForm />)
const input = screen.getByLabelText('名前:')
const button = screen.getByRole('button', { name: '送信' })
await userEvent.type(input, '太郎')
await userEvent.click(button)
expect(screen.getByText('こんにちは、太郎さん!')).toBeInTheDocument()
})
it('テスト②: 空欄で送信してもメッセージは表示されない', async () => {
render(<SimpleForm />)
const button = screen.getByRole('button', { name: '送信' })
await userEvent.click(button)
expect(screen.queryByText(/こんにちは、/)).not.toBeInTheDocument()
})
})
- テスト①: 名前を入力して送信するとメッセージが表示される
userEvent.type
: 実際のユーザーがキーボード入力するのに近い挙動でinput
に「太郎」と入力userEvent.click
: ボタンをクリック(= フォームの送信をトリガー)- 送信後に
submitted
がtrue
になり、<p>こんにちは、太郎さん!</p>
が表示されることを検証
- テスト② 空欄で送信してもメッセージは表示されない
- 何も入力せずにボタンだけをクリック(空欄送信)
/こんにちは、/
は「こんにちは、〇〇さん!」にマッチする正規表現
-「何も表示されていないこと(フォーム送信がされていない)」を確認
ユニットテストの基本:Server Actions 編
Server Actionsは、Next.js App Routerで使える新しい非同期処理の仕組み。
フォーム送信やボタンクリックなどのアクションを、クライアントから直接サーバーの関数に渡せるという特徴があります。
ただし、これはuse client
を使ったコンポーネントから呼び出される「async 関数」なので、テストでは普通のロジックとして扱えます。
サーバー側で登録する(仮)関数
テスト対象のコード
export async function submitName(name: string): Promise<string> {
if (!name.trim()) {
throw new Error('名前が空です')
}
// 実際はここにDB登録などの処理が入る
return `こんにちは、${name}さん!`
}
- 受け取った文字列が空の場合はエラーが投げられます。
- それ以外の場合は「こんにちは〜」から始まる文字列を返します。
テストコード
/**
* @jest-environment node
*/
import { submitName } from './submitName'
describe('submitName (Server Action)', () => {
it('正しい名前を送信したとき、メッセージが返る', async () => {
const result = await submitName('太郎')
expect(result).toBe('こんにちは、太郎さん!')
})
it('空欄を送信したとき、エラーが投げられる', async () => {
await expect(submitName('')).rejects.toThrow('名前が空です')
})
})
@jest-environment node
で、node環境でテストを動かします。.rejects.toThrow()
で非同期関数の例外処理をテストできます。
サーバー側で外部APIにアクセスしてデータを取得する関数
テスト対象のコード
export async function fetchPost(postId: number): Promise<string> {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
if (!res.ok) {
throw new Error('投稿の取得に失敗しました')
}
const post = await res.json()
return post.title
}
- JSON Placeholderを使って投稿を取得します。
- 取得できなかった場合はエラーを投げます。
テストコード
/**
* @jest-environment node
*/
import { fetchPost } from './fetchPost'
// fetch をモックする
global.fetch = jest.fn()
describe('fetchPost (Server Action with external API)', () => {
beforeEach(() => {
jest.resetAllMocks()
})
it('投稿IDを指定するとタイトルが返る', async () => {
const mockJson = jest.fn().mockResolvedValue({ id: 1, title: 'mock title' })
;(fetch as jest.Mock).mockResolvedValue({
ok: true,
json: mockJson,
})
const result = await fetchPost(1)
expect(result).toBe('mock title')
expect(fetch).toHaveBeenCalledWith('https://jsonplaceholder.typicode.com/posts/1')
})
it('APIのレスポンスがエラーだった場合、例外を投げる', async () => {
;(fetch as jest.Mock).mockResolvedValue({ ok: false })
await expect(fetchPost(99)).rejects.toThrow('投稿の取得に失敗しました')
})
})
ポイント | 解説 |
---|---|
global.fetch = jest.fn() |
Jest (またはVitest) のモックで fetch を上書き |
mockResolvedValue() |
非同期関数の成功時の戻り値を模倣 |
mockJson() |
res.json() も非同期なので別途モック |
.rejects.toThrow() |
エラー系の検証はこの書き方で安全に書けます |
resetAllMocks() |
テスト間でモック状態が混ざらないよう初期化します |
下記コマンドでテストを実行します。
npm run test
合計4ファイルのテストが正常終了しました。
統合テスト構成:App Routerとページ単位のテスト
Next.js App Router構成では、layout.tsx
, page.tsx
, template.tsx
などのファイルをルートごとに分けて管理するスタイルが主流です。
これにより、アプリケーション全体の構造は明確になりますが、その一方で「ページ全体を対象とした統合テストをどのように設計すればよいか」という点で悩む方も多いのではないでしょうか。
このセクションでは、App Router構成におけるページ単位の統合テストの基本的な進め方と、よく使われるモックの書き方について紹介します。
統合テストの目的とは?
ユニットテストが「コンポーネント単体の振る舞い」を検証するのに対し、統合テストでは主に以下のような確認を行います。
- ページコンポーネントと複数の子コンポーネントが正しく連携して動作するか
- クライアントコンポーネントや状態管理を伴う表示切り替えが期待通り行われるか
- ルーティング(
useRouter
,useSearchParams
)やサーバーアクションとの統合動作が想定通りか
「ユーザーがページを開いたとき、表示・状態・挙動が一通り正しく動くかをテストする」のが目的です。
page.tsx
テスト対象のコード
ダッシュボードを想定して新たにページを作成します。
このコンポーネントは、tab
クエリパラメータに応じて表示を切り替えるダッシュボードページです。
「設定タブへ」ボタンをクリックすると、URLが /dashboard?tab=settings
に変更され、表示も更新されるようになっています。
'use client'
import { useRouter, useSearchParams } from 'next/navigation'
import { Suspense } from 'react'
const TabContent = ({ tab }: { tab: string }) => {
switch (tab) {
case 'home':
return <p>ホームタブの内容</p>
case 'settings':
return <p>設定タブの内容</p>
default:
return <p>不明なタブです</p>
}
}
export default function DashboardPage() {
const router = useRouter()
const searchParams = useSearchParams()
const tab = searchParams.get('tab') || 'home'
const goToSettings = () => {
router.push('/dashboard?tab=settings')
}
return (
<main>
<h1>ダッシュボード</h1>
<button onClick={goToSettings} className='cursor-pointer'>設定タブへ</button>
<Suspense fallback={<p>読み込み中...</p>}>
<TabContent tab={tab} />
</Suspense>
</main>
)
}
-
useRouter()
- クライアントサイドルーティングを制御するフック。
router.push()
を使ってURLを変更できます(ページ遷移なし)。
-
useSearchParams()
- 現在のURLのクエリパラメータ(
?tab=home
など)を読み取るためのフック。 - クエリが存在しない場合は
null
を返すので、|| 'home'
でデフォルトを設定。
- 現在のURLのクエリパラメータ(
-
TabContent の構造と役割
- クエリパラメータで渡された tab に応じて、表示内容を切り替えています。
- このような表示切り替えは、UI上で「同じページ内で切り替える」場合に非常によく使われるパターンです。
テストコード
const pushMock = jest.fn()
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: pushMock,
}),
useSearchParams: () => new URLSearchParams('tab=home'),
}))
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import DashboardPage from '../page'
describe('DashboardPage', () => {
it('初期表示でホームタブが選択されていること', () => {
render(<DashboardPage />)
expect(screen.getByRole('heading', { name: 'ダッシュボード' })).toBeInTheDocument()
expect(screen.getByText('ホームタブの内容')).toBeInTheDocument()
})
it('「設定タブへ」ボタンをクリックすると router.push が呼ばれる', async () => {
render(<DashboardPage />)
const button = screen.getByRole('button', { name: '設定タブへ' })
await userEvent.click(button)
expect(pushMock).toHaveBeenCalledWith('/dashboard?tab=settings')
expect(pushMock).toHaveBeenCalledTimes(1)
})
})
jest.mock('next/navigation'
- 目的:Next.jsの
useRouter()
とuseSearchParams()
をモックに差し替えています。 pushMock
はrouter.push()
を呼んだかどうかを確認するための モック関数。useSearchParams()
は URL のクエリ?tab=home
を再現するため、常に"home"
を返すよう固定。- テストでは実際のルーター挙動は必要ないため、「こう動いていることにして検証する」=モック戦略が有効です。
- 目的:Next.jsの
- 初期表示でホームタブが選択されていること
- このテストで「ページが初期状態で正しく描画されている」ことを保証しています。
- render() で DashboardPage を仮想DOMに描画。
<h1>
の要素に "ダッシュボード" と書かれていることを確認。- クエリパラメータが
"tab=home"
なので、正しく内容が表示されているか検証。
- 「設定タブへ」ボタンをクリックすると router.push が呼ばれる
- 「ユーザー操作によりURL遷移の意図が正しくトリガーされた」ことを保証しています。
"設定タブへ"
というラベルのボタンを取得。userEvent.click()
でクリック操作を模擬。router.push('/dashboard?tab=settings')
が正しく呼ばれたかを確認。- 一度だけ呼ばれていることをチェック(二重実行されていないかの防止にもなる)
layout.tsx
Next.js App Routerの layout.tsx は、基本的に以下のような役割を持ちます
- 共通のレイアウト(Header、Sidebar、フッターなど)を描画する
- 子要素(children)を slot のように受け取って描画する
- グローバルなProviderをラップする(テーマ、認証、QueryClient など)
つまり、その中身は「props.childrenを含んだラッパーコンポーネント」にすぎません。そのため普通のReactコンポーネントとしてユニットテスト可能です。
テスト対象のコード
import { Header } from '@/components/Header'
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Header />
<main>{children}</main>
</>
)
}
import Link from 'next/link'
export function Header() {
return (
<header role="banner" className="bg-gray-800 text-white px-4 py-2">
<h1 className="text-xl font-bold">My App</h1>
<nav>
<ul className="flex gap-4 mt-2">
<li><Link href="/">ホーム</Link></li>
<li><Link href="/dashboard">ダッシュボード</Link></li>
</ul>
</nav>
</header>
)
}
テストコード
import { render, screen } from '@testing-library/react'
import RootLayout from '../layout'
describe('RootLayout', () => {
it('ヘッダーと children を表示する', () => {
render(
<RootLayout>
<p>これはテスト用の中身です</p>
</RootLayout>
)
// ヘッダーが表示されているか
expect(screen.getByRole('banner')).toBeInTheDocument()
// childrenが描画されているか
expect(screen.getByText('これはテスト用の中身です')).toBeInTheDocument()
})
})
下記コマンドでテストを実行します。
npm run test
合計6ファイルのテストが正常終了しました。
Playwright で E2Eテスト
Playwright で Next.js App をテストするメリット
1. 実際のブラウザ上で本番に近い動作を確認できる
- Playwright は Chrome / Firefox / Safari(WebKit)での動作を自動テストできます。
- App Routerの動的ルーティング・Server Actions・Layout構成など、ブラウザの挙動に依存する部分も正確に検証できます。
開発中は動いていたのに「本番ではクリックが効かない」みたいな事故を防げます。
2. ルーティング・ページ遷移も丸ごとテストできる
page.goto('/dashboard?tab=home')
で Next.js のページに直接アクセスawait page.click('text=設定タブへ')
でrouter.push()
の確認toHaveURL()
で URL の変化も検証できる
ユニットテストではできない「ページ単位の挙動保証」が可能になります。
3. Server Actions や APIレスポンスまで含めた統合確認ができる
- 入力 → form action={submit} → 結果が表示される
- この一連の動作をブラウザレベルで検証可能となります。
4. CI/CDと簡単に統合できる
- GitHub Actions, CircleCI, VercelのPreview Deployなどと連携して、自動でE2Eテストを動かすことができます。
playwright.config.ts
にwebServer
オプションを設定すれば、Next.js のdev
orstart
を自動で立ち上げてテストできます。
開発速度を落とさずに 安心してリリースできる仕組みが構築可能です。
5. ユニットテストだけでは拾えない不具合をキャッチ
ユニットテストで気づきにくいバグ | Playwrightなら気づける理由 |
---|---|
router.push() 後の表示更新がされない | 実際にボタンを押して URL が変わるか確認できる |
レイアウト・遷移の崩れ | 実ブラウザでレンダリングが走る |
Server Action でエラーが出てるのに気づかない | 実際にsubmitしてメッセージ表示されるかまで検証できる |
セットアップ手順
必要なパッケージのインストールをします。
npx playwright install
npm install --save-dev playwright @playwright/test
npx playwright install
- 各ブラウザ(ランタイム)をローカルにインストール
- Playwright が動作に必要なバイナリや依存ファイルを展開
playwright.config.ts
の設定
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './tests',
use: {
baseURL: 'http://localhost:3000',
video: 'retain-on-failure',
},
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: !process.env.CI,
},
})
testDir
- テストファイルの配置ディレクトリ(相対パス)
- 通常は tests/ や e2e/ が使われます
- この中に *.spec.ts や *.test.ts を置くことで自動的に認識されます
video: 'retain-on-failure'
- テストが失敗した際に動画を記録してあとで見ることができます。
- どのような状況でテストが失敗したか追いやすくなります。
webServer
- playwrightで操作する対象のサーバーを起動することができます。
command
- テスト前に Next.jsアプリ(開発サーバー)を起動するコマンド
port
- アプリが立ち上がるポートを指定
reuseExistingServer
- ローカルでは再利用、CIでは毎回起動 という設定
npm run dev
を何回も起動しなくて済むため、開発中は高速化できる
全体としての動きは以下の通りになります。
- テスト前に npm run dev をバックグラウンドで実行
- http://localhost:3000 が立ち上がるのを検知
- 各 .test.ts ファイルのテストを実行
- 終わったらサーバーを止める(または再利用)
.gitignore
動画の保存先を.gitignore
で除外しておきます。
# playwright
/test-results
実際のテストコード
ダッシュボード画面を作成したのでそのテストを実施します。
import { test, expect } from '@playwright/test'
test('ダッシュボードに My App のヘッダーが表示される', async ({ page }) => {
await page.goto('/dashboard')
// header が role="banner" で表示されていること
await expect(page.getByRole('banner')).toBeVisible()
// h1 内の "My App" テキストを確認したいならこちらもOK
await expect(page.getByRole('heading', { name: 'My App' })).toBeVisible()
})
test('「設定タブへ」をクリックするとURLが変わる', async ({ page }) => {
await page.goto('/dashboard?tab=home')
await page.getByRole('button', { name: '設定タブへ' }).click()
await expect(page).toHaveURL('/dashboard?tab=settings')
})
scriptsに追加
Playwright実行用のコマンドを追加します。
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"test": "jest",
+ "test:e2e": "playwright test",
"lint": "next lint"
},
下記コマンドでテストを実行します。
npm run test:e2e
2つのテストが正常終了しました。
おわりに
ここまで、Next.js App Router構成のプロジェクトにおけるユニットテスト・統合テスト・E2Eテストの基本と、React Testing Library・Jest・Playwrightを使った具体的な実装方法を紹介してきました。
特にApp Routerでは、Server ComponentやServer Action、ルーティングの設計がより柔軟になった一方で、テスト構成もそれに合わせて丁寧に設計する必要があります。
ですが、だからこそ「ユニットで細かく確認」「統合で構造を保証」「E2Eで体験全体をテスト」と、粒度に応じて戦略的にテストを書くことが、アプリの信頼性を大きく底上げしてくれます。
テストは「完璧に書く」ことが目的ではありません。今のアプリに必要な安心感や、開発体験の改善のために、少しずつ取り入れていけば大丈夫です。
みなさまのNext.jsアプリが、より安心して育てられるプロダクトになることを願っています。ここまで読んでいただき、本当にありがとうございました🙇
関連する技術ブログ
JestとTypeScriptで始めるテスト自動化:基本設定から型安全なテストの書き方まで徹底解説
JestとTypeScriptを使ったテスト自動化の基本を学びたい方へ。環境のセットアップ方法、型安全なテストを書くメリット、コードの信頼性を高める実践的なテクニックを初心者向けに丁寧に解説します。テストカバレッジの活用で、品質の高い開発を目指しましょう。
shinagawa-web.com
フロントエンド開発で欠かせないReactのUIコンポーネントのテストをReact Testing Libraryで実装
React Testing Libraryを使って、Reactコンポーネントのテストを行う方法を学びます。本記事では、Next.js環境でのセットアップから、ユーザーインタラクションをシミュレーションしたテストコードの作成までを詳しく解説します。コンポーネントが期待通りに動作するかを確認し、実際のアプリケーションに近い形でのテストを実装しましょう。
shinagawa-web.com
Playwrightを使ったE2Eテストを実装してシステム間の連携を含めたユーザー視点でのテストを実現
Playwrightを使用したエンドツーエンド(E2E)テストの導入方法から、サンプルコードの実行までを詳しく解説。クロスブラウザテストや並行テスト実行の特徴を活かし、効率的なテスト環境構築を学びましょう。
shinagawa-web.com
Webアクセシビリティの完全ガイド:Lighthouse / axe による自動テスト、WCAG基準策定、キーボード操作・スクリーンリーダー対応まで
Webアクセシビリティの課題を解決するための包括的なガイド。Lighthouse / axe を活用した自動テストの設定、WCAGガイドラインに基づく評価基準の整備、キーボード操作やスクリーンリーダー対応の改善、カラーコントラストの最適化、ARIAランドマークの導入、フォームやモーダルの操作性向上まで詳しく解説。定期的なアクセシビリティレポートを活用し、継続的な改善を実現する方法も紹介します。
shinagawa-web.com
フロントエンド開発に役立つモックサーバー構築:@graphql-tools/mock と Faker を使った実践ガイド
フロントエンド開発を先行させるために、バックエンドが未完成でもモックサーバーを立ち上げる方法を解説。@graphql-tools/mock と Faker を使用して、実際のデータに近い動作をシミュレートします。
shinagawa-web.com
Mock Service Worker (MSW) を使ったAPIモックとテストの効率化
MSW(Mock Service Worker)を使用して、フロントエンド開発やテスト環境でのAPIモックを効率的に行う方法を解説します。Mock Service Workerの基本的な使い方から、Jestテストでの活用方法、さらにテストを簡単にするための設定手順を紹介します。
shinagawa-web.com
Supertest と Jest を活用した Express + MongoDB アプリのエンドツーエンドテスト解説
Supertest と Jest を組み合わせて Express アプリケーションの API テストを効率化する方法を解説します。サービス層の導入やテスト可能なコード設計へのリファクタリング、GET, POST, PATCH, DELETEメソッドのテスト実装まで、具体的なコード例を交えて詳しく紹介します。
shinagawa-web.com
フロントエンドのテスト自動化戦略:Jest・Playwright・MSW を活用したユニット・E2E・API テスト最適化
フロントエンド開発において、品質を担保しながら効率的に開発を進めるためには、適切なテストの自動化が不可欠です。本記事では、Jest や Vitest を活用したユニットテストの導入・強化、React Testing Library や Storybook との統合によるコンポーネントテストの最適化、Playwright / Cypress を用いた E2E テストの拡充について詳しく解説します。さらに、Supertest や MSW を活用した API テストの自動化、Faker / GraphQL Mock によるモックデータの整理、CI/CD パイプラインにおける並列実行やキャッシュ活用による最適化など、テストを効果的に運用するための手法を紹介。また、Codecov / SonarQube によるテストカバレッジの可視化や、フィーチャーフラグを考慮したテスト戦略の策定についても解説し、実践的なアプローチを提案します。テストの信頼性と効率を向上させ、開発プロセスを強化したいフロントエンドエンジニア必見の内容です。
shinagawa-web.com
弊社の技術支援サービス
無駄なコストを削減し、投資対効果を最大化する
クラウド費用の高騰、不要なSaaSの乱立、開発工数の増加――これらの課題に悩んでいませんか?本サービスでは、クラウドコストの最適化、開発効率向上、技術選定の最適化 を通じて、単なるコスト削減ではなく、ROIを最大化する最適解 をご提案します。
shinagawa-web.com
最新技術の導入・検証を支援するPoCサービス
Remix、React Server Components、TypeScript移行、クラウドサービス比較、マイクロサービス、サーバーレス、デザインシステムなど、最新技術のPoC(概念実証)を通じて、最適な技術選定と導入を支援します。貴社の開発課題に合わせた検証・実装で、ビジネスの成長を加速させます。
shinagawa-web.com
開発生産性を最大化するための技術支援
開発チームの生産性向上、コードの品質管理、インフラの最適化まで、様々な側面からサポートします。コードベースのリファクタリングから、テスト自動化、オンボーディング強化まで、プロジェクトの成功に必要なすべての支援を提供。御社の開発現場が効率的に機能するように、技術的な障害を取り除き、スムーズな開発を実現します。
shinagawa-web.com
開発品質向上支援 – 効率的で安定したプロダクトを実現
フロントエンドからバックエンド、データベースまで、開発プロセス全体を最適化し、安定したプロダクト作りをサポートします。コードレビューの仕組み、型定義の強化、E2Eテスト環境の構築など、開発の各ステップにおけるベストプラクティスを導入することで、より効率的でバグの少ない、そしてユーザー満足度の高いサービス提供を支援します。
shinagawa-web.com
Webアプリのセキュリティ強化支援
Webアプリの脆弱性対策からインフラのセキュリティ強化まで、包括的なセキュリティ支援を提供。OWASP Top 10対策、JWT認証の最適化、APIのアクセス制御、依存パッケージの監査、セキュアコーディングの標準化など、実践的なアプローチで開発現場の安全性を向上させます。
shinagawa-web.com
目次
お問い合わせ