言語を切り替える
テーマを切り替える

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.tsmoduleNameMapper を追加します:

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 を実行します。緑色の PASS1 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('有効なメールアドレスを入力してください')
  })
})

テストの考え方

  1. render() でコンポーネントを描画
  2. screen.getByXxx() で要素を見つける(ロール、テキスト、プレースホルダーなどで)
  3. fireEvent でユーザー操作をシミュレート
  4. expect() で結果を断言

ちょっとしたコツ:getByTestId ではなく、できるだけ getByRole を使いましょう。role(buttonalert など)を使う方が、ユーザーが実際に見るものに近く、テストも壊れにくくなります。

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 エラーと解決策

問題 1Cannot 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.tsnext/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 パッケージ(nanoiduuid など)が原因なら、例外リストに追加:

transformIgnorePatterns: [
  'node_modules/(?!(nanoid|uuid)/)',
]

エラー 2:Unexpected token ‘export’

原因:上記と同様、変換されていないファイルがある。

解決策jest.config.tstransformtransformIgnorePatterns を確認。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.tsmoduleNameMapper を設定:

moduleNameMapper: {
  '^@/components/(.*)$': '<rootDir>/components/$1',
  '^@/lib/(.*)$': '<rootDir>/lib/$1',
  '^@/(.*)$': '<rootDir>/$1',
}

tsconfig.jsonpaths と一致させてください。

エラー 4:Objects are not valid as a React child

原因async Server Component をテストしている。Jest 非対応。

解決策

  1. 非同期ロジックを純粋関数に切り出して単体テスト
  2. E2E テスト(Playwright、Cypress)を使う

Server Components を Jest で無理にテストしない。ツールには限界があります。

エラー 5:act(…) warning

完全なエラー

Warning: An update to Component inside a test was not wrapped in act(...).

原因useEffectsetTimeout など非同期状態更新があり、テストが完了を待っていない。

解決策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 />)
  })
})

テストが遅い? 最適化のヒント

  1. 並列実行
{
  "scripts": {
    "test": "jest --maxWorkers=4"
  }
}
  1. 変更ファイルのみテスト
npm test -- --onlyChanged
  1. コードカバレッジを無効化(デバッグ時):
{
  "scripts": {
    "test:fast": "jest --no-coverage"
  }
}
  1. test.skip で遅いテストをスキップ
describe.skip('Slow Tests', () => {
  // これらのテストはスキップされる
})

問題診断チェックリスト

エラーが出たら、この順で確認:

  1. jest.config.tsnext/jest でラップされているか?
  2. jest.setup.ts@testing-library/jest-dom を正しくインポートしているか?
  3. ✅ パスエイリアス設定が tsconfig.json と一致しているか?
  4. ✅ Mock が必要なモジュール(next/navigationnext/image)は Mock 済みか?
  5. ✅ 非同期操作に waitFor または act を使っているか?
  6. ✅ 依存バージョンは互換性があるか(特に React 19 と Jest)?

このチェックリストを順に確認すれば、大半の問題は解決できます。

まとめ

テスト環境の構築は面倒に感じますが、一度動かせばコード品質は確実に上がります。

最初はテストを書くのが時間の無駄に思えるかもしれません。機能実装 5 分、テスト 10 分——そう感じる時期もありました。でもテストがあると、リファクタリングやバグ修正のとき心に余裕が生まれます。コードを直してテストを走らせ、全部緑なら安心。赤ければ、どこを壊したかすぐわかります。

おすすめの進め方

  • プロジェクトが大きくなってから始めるのではなく、今から新機能ごとにテストを書く習慣をつける。
  • 100% カバレッジを目指さなくていい。コアロジックとバグりやすい箇所を重点的にカバーすれば十分。
  • Server Components が Jest でテストできなくても無理しない。ロジックを切り出すか E2E で補完する。
  • エラーが出ても慌てない。この章のチェックリストで確認すれば、たいてい解決できる。

この記事の設定ファイルを保存しておけば、次の新プロジェクトでも 10 分でテスト環境を立ち上げられます。テストを書き始めれば、バグは減り、コード品質は自然と上がっていきます。

設定やテスト作成で困ったことがあれば、コメントで議論してください。私も同じ落とし穴を踏んできたので、できる限りお役に立てればと思います。

Next.js Jest テスト環境設定の完全フロー

Next.js 15 + Jest + React Testing Library のテスト環境をゼロから構築する手順

⏱️ 目安時間: 15 分

  1. 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

    ステップ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

    ステップ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

    ステップ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

    ステップ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 で設定をラップする必要があるのですか?
next/jest は Next.js 公式の 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 するには?
Next.js App Router のルーティング Hooks は 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」エラーが出るのはなぜ?
Jest がデフォルトで ES Modules をサポートしていないことが原因です。2 つの解決策があります。

方案一: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 するには?おすすめの方法は?
3 つの方法があり、複雑さ順に並べます。

方法一:グローバル 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 を推奨します。実際のネットワークリクエストに近く、再利用性が高く、チーム開発にも向いています。
テスト実行が遅い場合の最適化方法は?
4 つの実用的な方法があります。

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 ではフルテストを実行するのがおすすめです。
パスエイリアス @/ を設定してもテストでエラーになる場合は?
jest.config.ts と tsconfig.json のパス設定が一致しているか確認してください。

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日

関連記事

コメント

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