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'
そこから丸二日間、設定ファイルと Stack Overflow、GitHub Issues の間をひたすら往復する日々が始まりました。何通りもの設定を試し、jest.config.js を何度も書き直してようやくテストが通った瞬間は、キーボードを投げて喜びたい気分でした。
Next.js のテスト環境構築は複雑に見えますが、ポイントさえ押さえれば恐れる必要はありません。核心的な設定を理解して落とし穴を避ければ、実は10分ほどで動かせます。本記事では、Next.js 15 + Jest + React Testing Library の環境をゼロから構築する手順をまとめました。開発中に踏んだ地雷の数々や、Client/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: 'test@example.com' } })
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 === 'test@example.com') {
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)
jest.setup.ts で Mock Server を起動:
import '@testing-library/jest-dom'
import { server } from './mocks/server'
// テスト前に Mock Server を起動
beforeAll(() => server.listen())
// 各テスト後に handlers をリセット
afterEach(() => server.resetHandlers())
// テスト終了後に Server を停止
afterAll(() => server.close())
MSW の利点は、すべてのネットワークリクエストを傍受できるため、各テストで global.fetch を書く必要がないことです。
方法三:axios を Mock(axios 利用プロジェクト向け):
npm install -D axios-mock-adapter
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
const mock = new MockAdapter(axios)
describe('API Test', () => {
afterEach(() => {
mock.reset()
})
it('should fetch posts', async () => {
mock.onGet('/api/posts').reply(200, [{ id: 1, title: 'Test' }])
const response = await axios.get('/api/posts')
expect(response.data).toHaveLength(1)
})
})
環境変数の Mock
Next.js の環境変数もテストで Mock が必要になることがあります。
方法一:process.env を直接設定:
describe('Config Test', () => {
const originalEnv = process.env
beforeEach(() => {
jest.resetModules()
process.env = { ...originalEnv }
})
afterEach(() => {
process.env = originalEnv
})
it('should use API URL from env', () => {
process.env.NEXT_PUBLIC_API_URL = 'https://test-api.com'
const { getApiUrl } = require('@/lib/config')
expect(getApiUrl()).toBe('https://test-api.com')
})
})
方法二:.env.test ファイルを使う:
.env.test を作成すると、next/jest が自動でロードします:
NEXT_PUBLIC_API_URL=https://test-api.com
DATABASE_URL=postgresql://test:test@localhost:5432/test
サードパーティモジュールの Mock(Prisma の例)
Prisma で DB クエリをしている場合、テストで本番 DB に接続したくないでしょう。
Prisma Client の Mock:
// __mocks__/prisma.ts
export const prisma = {
user: {
findMany: jest.fn(),
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
post: {
findMany: jest.fn(),
create: jest.fn(),
},
}
テストでの使用例:
import { prisma } from '@/lib/prisma'
jest.mock('@/lib/prisma', () => ({
prisma: {
user: {
findUnique: jest.fn(),
},
},
}))
describe('getUserById', () => {
it('should return user', async () => {
const mockUser = { id: 1, name: 'Test User' }
;(prisma.user.findUnique as jest.Mock).mockResolvedValue(mockUser)
const result = await getUserById(1)
expect(result).toEqual(mockUser)
expect(prisma.user.findUnique).toHaveBeenCalledWith({
where: { id: 1 },
})
})
})
よくある Mock エラーと解決策
問題 1:Cannot find module 'next/router'
原因:Next.js ルーティングモジュールが Mock されていない。
解決:テストファイル先頭に jest.mock('next/navigation') を追加。
問題 2:Mock が効かない
原因:jest.mock の位置が誤っている。ファイル先頭(import の直後)に置く必要がある。
import { useRouter } from 'next/navigation'
// ここで Mock する
jest.mock('next/navigation')
describe('Test', () => {
// ...
})
問題 3:環境変数が読み込めない
原因:テストが .env をロードしていない。
解決:jest.config.ts が next/jest でラップされているか確認。自動で環境変数をロードします。
これらの Mock テクニックを押さえれば、Next.js 固有の機能もほとんどテストできます。
よくある問題のトラブルシューティング
Jest の設定中、エラーは日常茶飯事です。ただし 90% は同じパターンで、標準的な解決策があります。
エラー 1:Cannot use import statement outside a module
完全なエラー:
SyntaxError: Cannot use import statement outside a module
原因:Jest はデフォルトで ES Modules 非対応。コードまたは依存が import/export を使っている。
解決策:jest.config.ts に以下を追加:
const config: Config = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
transformIgnorePatterns: [
'node_modules/(?!(module-that-uses-esm)/)',
],
}
特定の npm パッケージ(nanoid、uuid など)が原因なら、例外リストに追加:
transformIgnorePatterns: [
'node_modules/(?!(nanoid|uuid)/)',
]
エラー 2:Unexpected token ‘export’
原因:上記と同様、変換されていないファイルがある。
解決策:jest.config.ts の transform と transformIgnorePatterns を確認。next/jest でラップされているか:
import nextJest from 'next/jest'
const createJestConfig = nextJest({ dir: './' })
export default createJestConfig(config)
next/jest が TypeScript と JSX の変換を自動処理します。
エラー 3:Cannot find module ’@/components/…’
原因:Jest がパスエイリアス(@/)を認識していない。
解決策:jest.config.ts に moduleNameMapper を設定:
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/lib/(.*)$': '<rootDir>/lib/$1',
'^@/(.*)$': '<rootDir>/$1',
}
tsconfig.json の paths と一致させてください。
エラー 4:Objects are not valid as a React child
原因:async Server Component をテストしている。Jest 非対応。
解決策:
- 非同期ロジックを純粋関数に切り出して単体テスト
- E2E テスト(Playwright、Cypress)を使う
Server Components を Jest で無理にテストしない。ツールには限界があります。
エラー 5:act(…) warning
完全なエラー:
Warning: An update to Component inside a test was not wrapped in act(...).
原因:useEffect や setTimeout など非同期状態更新があり、テストが完了を待っていない。
解決策:waitFor または act で非同期操作をラップ:
import { waitFor } from '@testing-library/react'
it('should update state', async () => {
render(<Component />)
await waitFor(() => {
expect(screen.getByText('Updated')).toBeInTheDocument()
})
})
または act を使う:
import { act } from '@testing-library/react'
it('should trigger callback', async () => {
await act(async () => {
render(<Component />)
})
})
テストが遅い? 最適化のヒント
- 並列実行:
{
"scripts": {
"test": "jest --maxWorkers=4"
}
}
- 変更ファイルのみテスト:
npm test -- --onlyChanged
- コードカバレッジを無効化(デバッグ時):
{
"scripts": {
"test:fast": "jest --no-coverage"
}
}
test.skipで遅いテストをスキップ:
describe.skip('Slow Tests', () => {
// これらのテストはスキップされる
})
問題診断チェックリスト
エラーが出たら、この順で確認:
- ✅
jest.config.tsがnext/jestでラップされているか? - ✅
jest.setup.tsが@testing-library/jest-domを正しくインポートしているか? - ✅ パスエイリアス設定が
tsconfig.jsonと一致しているか? - ✅ Mock が必要なモジュール(
next/navigation、next/image)は Mock 済みか? - ✅ 非同期操作に
waitForまたはactを使っているか? - ✅ 依存バージョンは互換性があるか(特に React 19 と Jest)?
このチェックリストを順に確認すれば、大半の問題は解決できます。
まとめ
テスト環境の構築は面倒に感じますが、一度動かせばコード品質は確実に上がります。
最初はテストを書くのが時間の無駄に思えるかもしれません。機能実装 5 分、テスト 10 分——そう感じる時期もありました。でもテストがあると、リファクタリングやバグ修正のとき心に余裕が生まれます。コードを直してテストを走らせ、全部緑なら安心。赤ければ、どこを壊したかすぐわかります。
おすすめの進め方:
- プロジェクトが大きくなってから始めるのではなく、今から新機能ごとにテストを書く習慣をつける。
- 100% カバレッジを目指さなくていい。コアロジックとバグりやすい箇所を重点的にカバーすれば十分。
- Server Components が Jest でテストできなくても無理しない。ロジックを切り出すか E2E で補完する。
- エラーが出ても慌てない。この章のチェックリストで確認すれば、たいてい解決できる。
この記事の設定ファイルを保存しておけば、次の新プロジェクトでも 10 分でテスト環境を立ち上げられます。テストを書き始めれば、バグは減り、コード品質は自然と上がっていきます。
設定やテスト作成で困ったことがあれば、コメントで議論してください。私も同じ落とし穴を踏んできたので、できる限りお役に立てればと思います。
Next.js Jest テスト環境設定の完全フロー
Next.js 15 + Jest + React Testing Library のテスト環境をゼロから構築する手順
⏱️ 目安時間: 15 分
- 1
ステップ1: テスト依存パッケージのインストール
Jest と React Testing Library 一式をインストールします。
• npm install -D jest jest-environment-jsdom
• npm install -D @testing-library/react @testing-library/dom @testing-library/jest-dom
• npm install -D ts-node @types/jest(TypeScript プロジェクトの場合)
パッケージの役割:
• jest:テストフレームワーク本体
• jest-environment-jsdom:ブラウザ DOM 環境のシミュレート
• @testing-library/react:React コンポーネントテストツール
• @testing-library/jest-dom:アサーション拡張(toBeInTheDocument など)
一括インストール後、すぐにテストは実行せず、先に設定ファイルを作成してください。 - 2
ステップ2: Jest 設定ファイルの作成
プロジェクトルートに jest.config.ts を作成し、next/jest で Next.js 固有の処理を自動化します。
```typescript
import type { Config } from 'jest'
import nextJest from 'next/jest'
const createJestConfig = nextJest({ dir: './' })
const config: Config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/lib/(.*)$': '<rootDir>/lib/$1',
},
}
export default createJestConfig(config)
```
next/jest が自動処理する内容:CSS/画像 Mock、環境変数ロード、TypeScript 変換、node_modules 除外。 - 3
ステップ3: Jest 起動設定の作成
プロジェクトルートに jest.setup.ts を作成し、Jest DOM 拡張をインポートします。
```typescript
import '@testing-library/jest-dom'
```
これにより以下のアサーションが使えます:
• expect(element).toBeInTheDocument()
• expect(element).toHaveClass('active')
• expect(element).toBeVisible()
• expect(element).toHaveTextContent('text')
このファイルをインポートしないと、上記メソッドは「not a function」エラーになります。 - 4
ステップ4: テストスクリプトの設定
package.json にテストコマンドを追加します。
```json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
```
3 つのコマンドの用途:
• npm test:全テスト実行(CI/CD 向け)
• npm run test:watch:監視モード、ファイル変更時に自動テスト(開発向け)
• npm run test:coverage:コードカバレッジレポート生成 - 5
ステップ5: サンプルテストで設定を検証
__tests__/example.test.ts を作成し、環境が正しく設定されたか確認します。
```typescript
describe('Example Test', () => {
it('should pass basic assertion', () => {
expect(1 + 1).toBe(2)
})
})
```
npm test を実行し、緑色の PASS が表示されれば設定成功です。
エラーが出た場合は順に確認:
1. jest.config.ts が createJestConfig でラップされているか
2. jest.setup.ts が正しくインポートされているか
3. package.json のテストスクリプトが正しいか
4. パスエイリアス設定が tsconfig.json と一致しているか
FAQ
なぜ next/jest で設定をラップする必要があるのですか?
• CSS と画像ファイルの自動 Mock(テストエラーを防ぐ)
• .env 環境変数の自動ロード
• TypeScript と JSX の自動変換
• node_modules と .next ディレクトリの自動除外
• Next.js Compiler の変換ルール設定
next/jest を使わない場合、上記をすべて手動設定する必要があり、非常に煩雑でエラーも起きやすくなります。公式は createJestConfig でラップすることを強く推奨しています。
Server Components は Jest でテストできますか?
• 同期 Server Components:通常どおりテスト可能
• async Server Components:Jest 非対応。「Objects are not valid as a React child」エラーになる
推奨アプローチ:
1. 非同期ロジックを純粋関数に切り出し、データ取得ロジックを単体テスト
2. Server Components はシンプルな描画のみに留める
3. Playwright や Cypress で E2E テストし、全体フローをカバー
すべての Server Components を Jest でテストしようとせず、適切なツールを選ぶことが大切です。
テスト時に Next.js の useRouter を Mock するには?
```typescript
import { useRouter } from 'next/navigation'
jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
usePathname: jest.fn(),
useSearchParams: jest.fn(),
}))
const pushMock = jest.fn()
;(useRouter as jest.Mock).mockReturnValue({
push: pushMock,
back: jest.fn(),
forward: jest.fn(),
})
```
jest.mock はテストファイル先頭(import の直後)に置き、describe や it の中には書かないでください。
「Cannot use import statement outside a module」エラーが出るのはなぜ?
方案一:transformIgnorePatterns を設定(推奨)
```typescript
const config: Config = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
transformIgnorePatterns: [
'node_modules/(?!(nanoid|uuid)/)',
],
}
```
方案二:next/jest で設定を正しくラップ
```typescript
import nextJest from 'next/jest'
const createJestConfig = nextJest({ dir: './' })
export default createJestConfig(config)
```
90% のケースは、ESM を使う npm パッケージ名を transformIgnorePatterns の例外リストに追加すれば解決します。
API リクエストを Mock するには?おすすめの方法は?
方法一:グローバル fetch の Mock(最もシンプル、小規模プロジェクト向け)
```typescript
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: 'test' }),
})
) as jest.Mock
```
方法二:MSW(Mock Service Worker、中〜大規模プロジェクト向け・推奨)
• すべてのネットワークリクエストを傍受
• 複雑なリクエスト/レスポンスロジックに対応
• 各テストで Mock を書く必要なし
• インストール:npm install -D msw
方法三:axios-mock-adapter(axios 利用プロジェクト向け)
• axios 専用 Mock ライブラリ
• API がシンプルで使いやすい
MSW を推奨します。実際のネットワークリクエストに近く、再利用性が高く、チーム開発にも向いています。
テスト実行が遅い場合の最適化方法は?
1. 並列実行(最も効果的)
```json
{ "scripts": { "test": "jest --maxWorkers=4" } }
```
2. 変更ファイルのみテスト
```bash
npm test -- --onlyChanged
```
3. コードカバレッジを無効化(デバッグ時)
```json
{ "scripts": { "test:fast": "jest --no-coverage" } }
```
4. 遅いテストをスキップ
```typescript
describe.skip('Slow E2E Tests', () => {
// これらのテストはスキップされる
})
```
開発時は --onlyChanged と --no-coverage、CI/CD ではフルテストを実行するのがおすすめです。
パスエイリアス @/ を設定してもテストでエラーになる場合は?
tsconfig.json(または jsconfig.json):
```json
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/components/*": ["components/*"],
"@/lib/*": ["lib/*"]
}
}
}
```
jest.config.ts:
```typescript
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/lib/(.*)$': '<rootDir>/lib/$1',
}
```
注意点:
1. tsconfig は相対パス(<rootDir> なし)
2. jest.config は絶対パス(<rootDir> 付き)
3. ワイルドカードの書き方を統一(.*)
修正後にテストを再起動すれば、パスエイリアスが認識されるはずです。
5分で読めます · 公開日: 2026年1月7日 · 更新日: 2026年6月8日
Next.js 完全ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Next.js リアルタイムチャット:WebSocket と SSE の正しい使い方
WebSocket、SSE、Long Polling の 3 つのリアルタイム通信方式を深く比較し、Vercel デプロイの実践知見を共有。Socket.io 統合、メッセージ状態管理、パフォーマンス最適化の完全なコード例付き。
第 20 / 47 記事
次の記事
Next.js 画像最適化完全攻略:Image コンポーネントの正しい使い方
Next.js Image コンポーネントの使用方法を完全解析。画像の読み込み遅延、リモート画像設定エラー、レイアウトシフト(CLS)などの問題を解決します。Next.js 14/15 の最新機能、実戦コード例、パフォーマンス最適化テクニックを含み、Web サイトのパフォーマンスを 60-80% 向上させます。
第 22 / 47 記事
関連記事
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js Middleware 実践ガイド:パスマッチ、Edge Runtime 制限とよくある落とし穴
コメント
GitHubアカウントでログインしてコメントできます