Next.js ユニットテスト実践:Jest + React Testing Library 完全設定ガイド

月曜の朝10時、コーヒーも飲み終わらないうちに、テックリーダーがチャットでこう言いました。「今週からプロジェクトにユニットテストを追加する。Next.js だから Jest で頼む。」私は画面を見つめ、指をキーボードの上に置いたまま固まりました——正直に言うと、頭の中は真っ白でした。React のテストは書いたことがありましたが、Next.js の App Router や Server Components をどうテストすればいいのか、全く見当もつきませんでした。
プロジェクトを開き、npm install jest を叩いて、自信満々に npm test を実行しました。すると、画面には赤いエラーの山が:
Error: Cannot use import statement outside a module
SyntaxError: Unexpected token 'export'
Cannot find module 'next/navigation'まるまる2日間、私は設定ファイルと Stack Overflow と GitHub Issues の間を行ったり来たりしました。十数種類の設定を試し、jest.config.js を数え切れないほど修正し、ようやくテストが通った瞬間、祝杯をあげてキーボードを投げ捨てそうになりました。
正直なところ、Next.js のテスト環境構築は想像以上に複雑です。しかし、ネットで言われているほど不可解なものでもありません——いくつかの核心的な設定を理解し、よくある落とし穴を避ければ、実は10分で動かせるようになります。この記事では、Next.js 15 + Jest + React Testing Library の環境をゼロから構築する方法を案内し、私が踏んだ落とし穴や、Client Components、Server Components、Hooks、API Mock のテスト方法も共有します。
もしあなたもテスト設定に頭を抱えているなら、この先を読み進めてください。
テスト環境構築(ゼロからスタート)
四の五の言わず、まずは環境を動かしましょう。退屈に見えるかもしれませんが、各設定項目の必要性を説明しますので、コピペして終わりにならないようにしましょう。
依存関係のインストール
ターミナルを開き、以下のパッケージを一気にインストールします:
npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom ts-node @types/jest各パッケージの役割を簡単に説明します:
- jest:テストフレームワーク本体
- jest-environment-jsdom:ブラウザ環境をシミュレート(React コンポーネントには DOM が必要)
- @testing-library/react:React コンポーネントテストツール
- @testing-library/jest-dom:追加のアサーションメソッド(
toBeInTheDocument()など) - ts-node と @types/jest:TypeScript サポート(JS ならスキップ可)
インストールが終わっても、まだテストは走らせないでください。設定ファイルがまだです。
jest.config.ts の作成
これが最も重要な設定ファイルです。プロジェクトルートに jest.config.ts を新規作成します(JS なら .js 拡張子):
import type { Config } from 'jest'
import nextJest from 'next/jest'
// この関数が Next.js の設定を自動的にロードします
const createJestConfig = nextJest({
dir: './', // Next.js プロジェクトルートディレクトリ
})
const config: Config = {
coverageProvider: 'v8', // コードカバレッジツール
testEnvironment: 'jsdom', // ブラウザ環境をシミュレート
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'], // テスト起動前の設定ファイル
}
// createJestConfig で設定をラップし、Next.js の各種変換を自動処理させる
export default createJestConfig(config)ここが重要:なぜ next/jest で設定をラップするのか?
next/jest は以下のことを自動でやってくれます:
.css、.module.cssファイルの処理(自動 Mock。これがないとテストがエラーになる)- 画像、フォントなどの静的リソースの処理
.env環境変数のロード- TypeScript と JSX の変換
node_modulesと.nextディレクトリの除外
これを使わないと、これら全てを手動で設定することになります。信じてください、それは苦行です。
jest.setup.ts の作成
ルートディレクトリにもう一つ jest.setup.ts ファイルを作成します。中身は超シンプルです:
import '@testing-library/jest-dom'この1行が Jest DOM のカスタムマッチャーをインポートし、以下のような断言を使えるようにします:
expect(element).toBeInTheDocument()expect(element).toHaveClass('active')expect(element).toBeVisible()
このファイルがないと、上記のようなメソッドは認識されません。
パスエイリアスの設定(@/ を使用している場合)
もしプロジェクトで import Button from '@/components/Button' のようなパスエイリアスを使用している場合、Jest にパスの解決方法を教える必要があります。
まず tsconfig.json(または jsconfig.json)に以下のような設定があるか確認してください:
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/components/*": ["components/*"],
"@/lib/*": ["lib/*"]
}
}
}その場合、jest.config.ts に moduleNameMapper を追加します:
const config: Config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
// この部分を追加
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/lib/(.*)$': '<rootDir>/lib/$1',
},
}なぜこれが必要か? Jest はデフォルトでは @/ というパスを認識せず、相対パスか絶対パスしか理解しません。「@/components/Button を見たら <rootDir>/components/Button を探しに行け」と教えてあげる必要があります。
テストスクリプトの追加
最後に、package.json に2つのスクリプトを追加します:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}
}npm test:全テストを一度実行npm test:watch:監視モード。ファイル変更時に自動で再テスト
設定の検証
さあ、簡単なテストを走らせて設定がOKか確認しましょう。プロジェクト内に __tests__/example.test.ts を作成します:
describe('Example Test', () => {
it('should pass', () => {
expect(1 + 1).toBe(2)
})
})npm test を実行します。緑色の PASS と 1 passed が表示されれば、おめでとうございます、設定成功です。
もしエラーが出ても慌てないでください。第5章のトラブルシューティングを見てください。エラーの 90% はそこに解決策があります。
コンポーネントテスト実践
設定ができたら、本物のテストを書きましょう。コンポーネントテストは Next.js テストの核心ですが、Client Components と Server Components ではテスト方法が全く異なります。
Client Components テスト(標準フロー)
Client Components は、おなじみの 'use client' がついたコンポーネントです。テスト方法は非常に直感的です。
ログインフォームコンポーネント LoginForm.tsx があるとします:
'use client'
import { useState } from 'react'
export default function LoginForm() {
const [email, setEmail] = useState('')
const [error, setError] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!email.includes('@')) {
setError('有効なメールアドレスを入力してください')
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="メールアドレス"
/>
{error && <span role="alert">{error}</span>}
<button type="submit">ログイン</button>
</form>
)
}テストファイル LoginForm.test.tsx:
import { render, screen, fireEvent } from '@testing-library/react'
import LoginForm from '@/components/LoginForm'
describe('LoginForm', () => {
it('should render input and button', () => {
render(<LoginForm />)
// 入力欄の存在確認
const emailInput = screen.getByPlaceholderText('メールアドレス')
expect(emailInput).toBeInTheDocument()
// ボタンの存在確認
const submitButton = screen.getByRole('button', { name: 'ログイン' })
expect(submitButton).toBeInTheDocument()
})
it('should show error for invalid email', () => {
render(<LoginForm />)
const emailInput = screen.getByPlaceholderText('メールアドレス')
const submitButton = screen.getByRole('button', { name: 'ログイン' })
// ユーザー入力をシミュレート
fireEvent.change(emailInput, { target: { value: 'invalid-email' } })
fireEvent.click(submitButton)
// エラーメッセージを確認
const errorMessage = screen.getByRole('alert')
expect(errorMessage).toHaveTextContent('有効なメールアドレスを入力してください')
})
})テストの考え方:
render()でコンポーネントを描画screen.getByXxx()で要素を見つける(ロール、テキスト、プレースホルダーなどで)fireEventでユーザー操作をシミュレートexpect()で結果を断言
ちょっとしたコツ:getByTestId ではなく、できるだけ getByRole を使いましょう。role(button、alert など)を使う方が、ユーザーが実際に見るものに近く、テストも壊れにくくなります。
Server Components テスト(ちょっと厄介)
Server Components は Next.js 15 の核心機能ですが、正直なところ Jest のサポートはあまり優しくありません。
核心的な問題:Jest は async Server Components をサポートしていません。
例えば、データベースからデータを取得するコンポーネントがあるとします:
// app/posts/page.tsx (Server Component)
async function getPosts() {
const res = await fetch('https://api.example.com/posts')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}このコンポーネントを直接テストすると、Jest はエラーを吐きます:Objects are not valid as a React child。
どうすればいいか?
3つのアプローチがあります:
案1:ビジネスロジックを切り出して純粋関数としてテスト
// lib/posts.ts
export async function getPosts() {
const res = await fetch('https://api.example.com/posts')
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
}
// lib/posts.test.ts
import { getPosts } from './posts'
describe('getPosts', () => {
it('should fetch posts successfully', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve([{ id: 1, title: 'Test Post' }]),
})
) as jest.Mock
const posts = await getPosts()
expect(posts).toHaveLength(1)
expect(posts[0].title).toBe('Test Post')
})
})こうすれば、コンポーネント自体ではなくデータ取得ロジックをテストすることになります。要するに、複雑な非同期ロジックを抜き出して単体テストし、コンポーネントには単純な描画だけを残すのです。
案2:同期的な Server Components をテスト
Server Component が非同期処理を含まないなら、普通にテストできます:
// components/Title.tsx (Server Component, asyncなし)
export default function Title({ text }: { text: string }) {
return <h1 className="title">{text}</h1>
}
// components/Title.test.tsx
import { render, screen } from '@testing-library/react'
import Title from './Title'
describe('Title', () => {
it('should render title', () => {
render(<Title text="Hello World" />)
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toHaveTextContent('Hello World')
})
})案3:E2E テストで補完
複雑な Server Components については、正直なところ Playwright や Cypress で E2E テストをする方が確実です。Jest 単体テストはフロントエンドロジックを担当し、E2E テストは全体フローを担当する、というふうに役割分担します。
私のやり方はこうです:核心的なビジネスロジックは純粋関数として切り出してテストし、Server Components は単純な描画のみにして、E2E テストでカバーする。
インタラクションテストのテクニック
ユーザー操作をテストする際、@testing-library/react は多くのメソッドを提供しています:
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
// クリック
fireEvent.click(button)
// 入力
fireEvent.change(input, { target: { value: 'test' } })
// 非同期更新を待つ
await waitFor(() => {
expect(screen.getByText('Success')).toBeInTheDocument()
})
// 要素が表示されているか
expect(element).toBeVisible()
// クラスを持っているか
expect(element).toHaveClass('active')完全な非同期インタラクションテストの例:
it('should submit form successfully', async () => {
// API を Mock
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ success: true }),
})
) as jest.Mock
render(<LoginForm />)
const emailInput = screen.getByPlaceholderText('メールアドレス')
const submitButton = screen.getByRole('button', { name: 'ログイン' })
fireEvent.change(emailInput, { target: { value: '[email protected]' } })
fireEvent.click(submitButton)
// 成功メッセージが出るのを待つ
await waitFor(() => {
expect(screen.getByText('ログイン成功')).toBeInTheDocument()
})
})waitFor に注目してください。これは非同期操作が完了するのを待ってからチェックを行います。もしコンポーネントに useEffect や非同期の状態更新がある場合、これを使わないと、状態が更新される前に断言が実行されてしまい、テストが失敗します。
Hook テスト戦略
カスタム Hook は React の精髄ですが、どうテストすればいいでしょうか? コンポーネント内で使ってテストするという人もいますが、それだと Hook のロジックとコンポーネントのロジックが混ざってしまいます。より良い方法は renderHook を使うことです。
単純な Hook のテスト
カウンター Hook を書いたとします:
// hooks/useCounter.ts
import { useState } from 'react'
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue)
const increment = () => setCount(c => c + 1)
const decrement = () => setCount(c => c - 1)
const reset = () => setCount(initialValue)
return { count, increment, decrement, reset }
}テストコード:
// hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('should initialize with default value', () => {
const { result } = renderHook(() => useCounter())
expect(result.current.count).toBe(0)
})
it('should initialize with custom value', () => {
const { result } = renderHook(() => useCounter(10))
expect(result.current.count).toBe(10)
})
it('should increment count', () => {
const { result } = renderHook(() => useCounter())
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
it('should reset count', () => {
const { result } = renderHook(() => useCounter(5))
act(() => {
result.current.increment()
result.current.increment()
})
expect(result.current.count).toBe(7)
act(() => {
result.current.reset()
})
expect(result.current.count).toBe(5)
})
})重要ポイント:
renderHookで Hook をレンダリングactで状態更新操作をラップする(React のルール。状態更新完了を保証する)result.currentで Hook の戻り値を取得
Context に依存する Hook のテスト
Hook が Context(例えば Auth Context)に依存している場合、Provider を提供する必要があります。
// hooks/useAuth.ts
import { useContext } from 'react'
import { AuthContext } from '@/contexts/AuthContext'
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}テスト時に Mock Provider を提供します:
// hooks/useAuth.test.tsx
import { renderHook } from '@testing-library/react'
import { useAuth } from './useAuth'
import { AuthContext } from '@/contexts/AuthContext'
describe('useAuth', () => {
it('should return auth context value', () => {
const mockAuthValue = {
user: { id: 1, name: 'Test User' },
login: jest.fn(),
logout: jest.fn(),
}
const wrapper = ({ children }: { children: React.ReactNode }) => (
<AuthContext.Provider value={mockAuthValue}>
{children}
</AuthContext.Provider>
)
const { result } = renderHook(() => useAuth(), { wrapper })
expect(result.current.user).toEqual({ id: 1, name: 'Test User' })
expect(result.current.login).toBeDefined()
})
it('should throw error when used outside provider', () => {
// エラーをキャッチ
const { result } = renderHook(() => useAuth())
expect(result.error).toEqual(
Error('useAuth must be used within AuthProvider')
)
})
})テクニック:wrapper パラメータで Provider をラップすることで、Hook が Context にアクセスできるようになります。
非同期 Hook(データ取得)のテスト
データ取得用の Hook はよくあります:
// hooks/useFetch.ts
import { useState, useEffect } from 'react'
export function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
const response = await fetch(url)
if (!response.ok) throw new Error('Network error')
const json = await response.json()
setData(json)
} catch (err) {
setError(err as Error)
} finally {
setLoading(false)
}
}
fetchData()
}, [url])
return { data, loading, error }
}非同期 Hook のテストには、fetch の Mock と状態更新の待機が必要です:
// hooks/useFetch.test.ts
import { renderHook, waitFor } from '@testing-library/react'
import { useFetch } from './useFetch'
describe('useFetch', () => {
beforeEach(() => {
// 各テスト前に fetch Mock をリセット
jest.resetAllMocks()
})
it('should fetch data successfully', async () => {
const mockData = { id: 1, title: 'Test' }
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockData),
})
) as jest.Mock
const { result } = renderHook(() => useFetch('/api/data'))
// 初期状態:loading = true
expect(result.current.loading).toBe(true)
expect(result.current.data).toBeNull()
// データロード完了を待つ
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.data).toEqual(mockData)
expect(result.current.error).toBeNull()
})
it('should handle fetch error', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: false,
})
) as jest.Mock
const { result } = renderHook(() => useFetch('/api/data'))
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.error).toBeTruthy()
expect(result.current.error?.message).toBe('Network error')
expect(result.current.data).toBeNull()
})
})注意点:
waitForで非同期操作完了を待つbeforeEachで Mock をリセットし、テスト間の干渉を防ぐ- 成功と失敗の両方のケースをテストする
Hook テストの極意は、依存関係のシミュレート、状態変更のトリガー、結果の断言です。この3ステップをマスターすれば、どんな Hook でもテストできます。
Mock テクニック大全
Mock はテストの魂です。Mock なしでは、テストは実際の API やデータベース、外部サービスに依存することになり、遅くて不安定になります。Next.js には Mock が必要な特別なものがいくつかあります。ここではそれらを攻略します。
Next.js ルーティングの Mock(一番よく使う)
Next.js のルーティング Hooks(useRouter, usePathname, useSearchParams)は、テスト環境ではデフォルトで利用できないため、Mock が必須です。
useRouter(App Router)の Mock:
// __mocks__/next/navigation.ts
export const useRouter = jest.fn()
export const usePathname = jest.fn()
export const useSearchParams = jest.fn()テストファイルでの使用:
import { useRouter } from 'next/navigation'
// ルーティングの振る舞いを Mock
jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
usePathname: jest.fn(),
useSearchParams: jest.fn(),
}))
describe('NavigationComponent', () => {
it('should navigate to home on button click', () => {
const pushMock = jest.fn()
;(useRouter as jest.Mock).mockReturnValue({
push: pushMock,
back: jest.fn(),
forward: jest.fn(),
})
render(<NavigationComponent />)
const button = screen.getByRole('button', { name: 'ホームへ戻る' })
fireEvent.click(button)
expect(pushMock).toHaveBeenCalledWith('/')
})
})usePathname(現在のパス取得)の Mock:
import { usePathname } from 'next/navigation'
jest.mock('next/navigation', () => ({
usePathname: jest.fn(),
}))
describe('HeaderComponent', () => {
it('should highlight active nav item', () => {
;(usePathname as jest.Mock).mockReturnValue('/about')
render(<Header />)
const aboutLink = screen.getByRole('link', { name: '阿部について' })
expect(aboutLink).toHaveClass('active')
})
})Next.js Image コンポーネントの Mock
next/image は Next.js の画像最適化サービスに依存しているため、テスト環境ではエラーになります。
案1:普通の img タグとして Mock:
// __mocks__/next/image.tsx
const Image = ({ src, alt }: { src: string; alt: string }) => {
return <img src={src} alt={alt} />
}
export default Image案2:jest.config.ts でグローバルに Mock:
const config: Config = {
// ... 他の設定
moduleNameMapper: {
'^next/image$': '<rootDir>/__mocks__/next/image.tsx',
},
}こうすれば、next/image を使っているすべての場所で自動的に Mock 版に置き換わります。
API リクエストの Mock(3つの方法)
方法1:グローバル fetch の Mock(一番簡単):
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: 'mock data' }),
})
) as jest.Mock方法2:MSW(Mock Service Worker)の使用(より強力):
npm install -D msw// mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/posts', () => {
return HttpResponse.json([
{ id: 1, title: 'Test Post' },
])
}),
http.post('/api/login', async ({ request }) => {
const { email } = await request.json()
if (email === '[email protected]') {
return HttpResponse.json({ success: true })
}
return HttpResponse.json({ error: 'Invalid email' }, { status: 400 })
}),
]// mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)テストセットアップで MSW を起動:
// jest.setup.ts
import { server } from './mocks/server'
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())MSW のメリットは、ネットワークレベルでリクエストを傍受するため、fetch を書き換える必要がなく、よりリアルに近いテストができる点です。
よくあるエラーとトラブルシューティング
最後に、私がテスト設定で遭遇したエラートップ3を紹介します。
エラー1:Cannot find module ’@/components/Button’
- 原因:Jest がパスエイリアスを認識していない。
- 解決策:
jest.config.tsのmoduleNameMapperにエイリアス設定を追加する。
エラー2:SyntaxError: Unexpected token ‘export’
- 原因:
node_modules内のライブラリがトランスパイルされていない。 - 解決策:
next/jestを使っているか確認。それでもだめならtransformIgnorePatternsでそのライブラリを除外リストから外す。
エラー3:act(…) warning
- 原因:状態更新が
act内で行われていない、または非同期操作をwaitForで待っていない。 - 解決策:非同期操作の後に
await waitFor(...)を追加するか、状態更新操作をact(...)でラップする。
まとめ
Next.js プロジェクトでのテスト環境構築は、最初は設定ファイルや Mock に圧倒されるかもしれませんが、一度セットアップしてしまえば、開発の信頼性を大きく高めてくれます。特にリファクタリング時には、テストがあるのとないのとでは安心感が段違いです。
まずは jest.config.ts を設定し、小さなコンポーネントからテストを書き始めてみてください。テストを書く習慣がつくと、コードの設計自体も自然ときれいになっていくはずです。
Next.js Jest テスト環境構築ステップ
Next.js 15 プロジェクトに Jest と React Testing Library をセットアップし、コンポーネントテストを実行するまでの完全ガイド
⏱️ Estimated time: 15 min
- 1
Step1: 依存関係のインストール
npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom ts-node @types/jest コマンドを実行し、必要なテストライブラリをインストールします。 - 2
Step2: jest.config.ts の作成
プロジェクトルートに設定ファイルを作成し、next/jest を使用して Next.js の特有の設定(CSS、画像処理、TypeScript変換など)を自動処理するように構成します。 - 3
Step3: jest.setup.ts の作成
ルートディレクトリに jest.setup.ts を作成し、import '@testing-library/jest-dom' を追加して、toBeInTheDocument などの便利な断言メソッドを使用可能にします。 - 4
Step4: パスエイリアスの設定
jest.config.ts の moduleNameMapper セクションで、@/components/* などのパスエイリアスを実際のディレクトリパス <rootDir>/components/* にマッピングします。 - 5
Step5: テストスクリプトの追加
package.json に "test": "jest" と "test:watch": "jest --watch" スクリプトを追加し、npm test でテストを実行できるようにします。
FAQ
Server Components を Jest でテストするとエラーになるのはなぜですか?
useRouter や usePathname をテストで使うとエラーになります。
Jest と Vitest、Next.js にはどちらがいいですか?
5 min read · 公開日: 2026年1月7日 · 更新日: 2026年1月22日
関連記事
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践

Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド

Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js E2E テスト:Playwright 自動化テスト実践ガイド


コメント
GitHubアカウントでログインしてコメントできます