Next.js: ユニットテスト & 統合テスト

最終更新:2026-08-26

テストはオプションではありません。本番レベルのアプリケーションの「エアバッグ」です。普段は必要ありませんが、いざという時に命を救います。

1. 学習目標



2. フルスタックエンジニアの実話

(1) 課題: ローンチ前夜、1 つのスペースで決済ページがクラッシュ

Alice は中東市場向けの EC プラットフォームでフルスタックエンジニアとして働いています。プラットフォームは 1 日 5 万件以上の注文を処理し、チームは週 2 回のリリースサイクルを維持しています。

先週金曜日のローンチで、一見無害な変更 — OrderSummary コンポーネントの価格フォーマット関数に余分なスペースを追加したこと — により、サウジアラビアのユーザーの支払い金額に小数点が 1 桁余分に表示されました。データ自体は実際には正しくありませんでしたが、カスタマーサービスに 200 件以上の苦情が寄せられ、3 人のユーザーが注文をキャンセルしました。

さらに悪いことに:

Alice は決意しました: このような問題が再発しないように、テスト体制を確立しなければなりません。

(2) Vitest + Testing Library の解決策

Alice はプロジェクトに Vitest テストスタックを導入しました:

BASH
npm install -D vitest @testing-library/react @testing-library/jest-dom @vitejs/plugin-react msw

そして最初のテストを作成しました:

TSX
import { render, screen } from '@testing-library/react'
import { OrderSummary } from './OrderSummary'

it('価格が正しくフォーマットされて表示される', () => {
  render(<OrderSummary total={99.99} currency="SAR" />)
  expect(screen.getByText(/99\.99/)).toBeInTheDocument()
})

(3) 効果

観点 Before After
コードカバレッジ < 5% > 75%
リリース前リグレッションテスト なし 5 分で自動実行
本番バグ発生率 月 8–12 件 月 0–2 件
新機能リリースの自信 低い 高い


3. Vitest テスト環境のセットアップ

Vitest は Vite エコシステムのネイティブテストフレームワークであり、Next.js 16(内部的に Turbopack と Vite を使用)とネイティブに互換性があります。

100%
graph TB
    A[vitest.config.ts] --> B[React プラグイン<br/>@vitejs/plugin-react]
    A --> C[テストグローバル<br/>globals:true]
    A --> D[環境<br/>jsdom]
    A --> E[セットアップファイル<br/>setup-test.ts]
    E --> F[jest-dom マッチャー]
    E --> G[MSW 起動]
    
    style A fill:#cce5ff
    style F fill:#d4edda
    style G fill:#d4edda
設定ファイル 目的 主要オプション
vitest.config.ts テスト主要設定 environment: 'jsdom' ブラウザシミュレーション
setup-test.ts グローバル初期化 jest-dom のインポート、MSW サービスの起動
tsconfig.json 型サポート types: ['vitest/globals']

(1) vitest.config.ts の設定

TS
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './src/__tests__/setup-test.ts',
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov'],
      thresholds: {
        statements: 70,
        branches: 60,
        functions: 70,
        lines: 70
      }
    }
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  }
})

(2) グローバルセットアップファイル

TS
// src/__tests__/setup-test.ts
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach, vi } from 'vitest'

afterEach(() => {
  cleanup()
})

vi.mock('next/navigation', () => ({
  useRouter: () => ({
    push: vi.fn(),
    replace: vi.fn(),
    refresh: vi.fn(),
    back: vi.fn(),
    forward: vi.fn()
  }),
  usePathname: () => '/',
  useSearchParams: () => new URLSearchParams()
}))

vi.mock('react-dom', async () => {
  const actual = await vi.importActual('react-dom')
  return { ...actual, useFormState: vi.fn() }
})

▶ サンプル: Vitest の動作確認

TSX
// src/__tests__/basic.test.ts
import { render, screen } from '@testing-library/react'

function Hello({ name }: { name: string }) {
  return <h1>こんにちは、{name}!</h1>
}

it('挨拶メッセージをレンダリングする', () => {
  render(<Hello name="Alice" />)
  expect(screen.getByText('こんにちは、Alice!')).toBeInTheDocument()
})
BASH
npx vitest run
💻 出力:

TEXT 📖 参照専用
 ✓ src/__tests__/basic.test.ts (1 test) 12ms

 Test Files  1 passed (1)
      Tests  1 passed (1)

Output:

TEXT 📖 参照専用
 ✓ src/__tests__/basic.test.ts (1 test) 12ms

 Test Files  1 passed (1)
       Tests  1 passed (1)

▶ サンプル: package.json へのテストスクリプトの追加

JSON
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage"
  }
}

Output:

TEXT 📖 参照専用
JSON structure defining three npm scripts: "test" (vitest run), "test:watch" (vitest), and "test:coverage" (vitest run --coverage).


4. @testing-library/react コンポーネントテスト

Testing Library の核心理念: 実装の詳細ではなく、ユーザーが見て操作するものをテストします。

(1) "render" と "screen" のクエリ

100%
graph LR
    A[render コンポーネント] --> B[screen 検索]
    B --> C{クエリタイプ}
    C --> D[getByText テキスト]
    C --> E[getByRole セマンティクス]
    C --> F[getByTestId テスト ID]
    C --> G[getByPlaceholderText プレースホルダー]
    
    D --> H[アサーション expect]
    E --> H
    F --> H
    G --> H
クエリメソッド 適用シナリオ
getByText テキスト内容 getByText('注文を送信')
getByRole セマンティック要素 getByRole('button', { name: /送信/i })
getByPlaceholderText 入力フィールドのヒント getByPlaceholderText('メールアドレスを入力')
getByTestId 非セマンティック要素 getByTestId('order-total')
queryByText 否定アサーション expect(queryByText('エラー')).not.toBeInTheDocument()

(2) サーバーコンポーネントのテスト(同期的レンダリング)

サーバーコンポーネントはクライアントサイド JavaScript を必要としません。テスト中に HTML 出力を直接アサーションできます:

TSX
// src/app/products/page.tsx
async function ProductsPage() {
  const res = await fetch('https://api.example.com/products')
  const products = await res.json()
  
  return (
    <ul>
      {products.map((p: { id: number; name: string; price: number }) => (
        <li key={p.id} data-testid="product-item">
          {p.name} — ${p.price}
        </li>
      ))}
    </ul>
  )
}

export default ProductsPage
TSX
// src/__tests__/products-page.test.tsx
import { render, screen } from '@testing-library/react'
import ProductsPage from '@/app/products/page'

// グローバル fetch をモック
global.fetch = vi.fn().mockResolvedValue({
  json: () => Promise.resolve([
    { id: 1, name: 'iPhone 16', price: 999 },
    { id: 2, name: 'Samsung S26', price: 899 }
  ])
})

it('サーバーから製品リストをレンダリングする', async () => {
  const page = await ProductsPage()
  render(page)
  
  expect(screen.getByText('iPhone 16')).toBeInTheDocument()
  expect(screen.getByText('Samsung S26')).toBeInTheDocument()
  expect(screen.getAllByTestId('product-item')).toHaveLength(2)
})

▶ サンプル: クライアントコンポーネントのテスト(ユーザー操作を含む)

Output:

TEXT 📖 参照専用
Renders the Component component UI.
TSX
// src/components/Counter.tsx
'use client'

import { useState } from 'react'

export function Counter({ initial = 0 }) {
  const [count, setCount] = useState(initial)
  
  return (
    <div>
      <p data-testid="count">カウント: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>増加</button>
      <button onClick={() => setCount(c => c - 1)}>減少</button>
    </div>
  )
}

Output:

TEXT 📖 参照専用
An interactive component with state management.
Visible text: カウント: {count}
TSX
// src/__tests__/counter.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Counter } from '@/components/Counter'

describe('Counter', () => {
  it('初期値でレンダリングされる', () => {
    render(<Counter initial={5} />)
    expect(screen.getByTestId('count')).toHaveTextContent('カウント: 5')
  })
  
  it('ボタンクリックで増加する', () => {
    render(<Counter initial={0} />)
    fireEvent.click(screen.getByText('増加'))
    expect(screen.getByTestId('count')).toHaveTextContent('カウント: 1')
  })
  
  it('ボタンクリックで減少する', () => {
    render(<Counter initial={10} />)
    fireEvent.click(screen.getByText('減少'))
    expect(screen.getByTestId('count')).toHaveTextContent('カウント: 9')
  })
})


5. jest-dom のカスタムマッチャー

jest-dom はセマンティックな DOM アサーションマッチャーを提供し、テストコードをより自然言語に近づけます。

マッチャー 機能
toBeInTheDocument() DOM 内の要素 expect(el).toBeInTheDocument()
toHaveTextContent(text) テキスト内容の一致 expect(el).toHaveTextContent('こんにちは')
toBeVisible() 要素が表示されている expect(el).toBeVisible()
toBeDisabled() ボタンが無効化されている expect(btn).toBeDisabled()
toHaveClass(cls) CSS クラスの一致 expect(el).toHaveClass('active')
toHaveAttribute(attr) 属性の一致 expect(input).toHaveAttribute('type', 'email')
toHaveValue(val) フォーム値の一致 expect(input).toHaveValue('test@example.com')

▶ サンプル: フォームバリデーションテスト

Output:

TEXT 📖 参照専用
Renders the Component component UI.
TSX
import { render, screen, fireEvent } from '@testing-library/react'
import { LoginForm } from '@/components/LoginForm'

describe('LoginForm バリデーション', () => {
  it('空のメールアドレスでエラーを表示する', () => {
    render(<LoginForm />)
    fireEvent.click(screen.getByRole('button', { name: /ログイン/i }))
    expect(screen.getByText(/メールアドレスを入力してください/i)).toBeInTheDocument()
    expect(screen.getByText(/メールアドレスを入力してください/i)).toBeVisible()
  })
  
  it('読み込み中は送信ボタンを無効化する', () => {
    render(<LoginForm />)
    fireEvent.change(screen.getByPlaceholderText('メールアドレスを入力'), {
      target: { value: 'alice@example.com' }
    })
    fireEvent.click(screen.getByRole('button', { name: /ログイン/i }))
    expect(screen.getByRole('button', { name: /ログイン中.../i })).toBeDisabled()
  })
  
  it('有効な入力後にエラーをクリアする', () => {
    render(<LoginForm />)
    fireEvent.click(screen.getByRole('button', { name: /ログイン/i }))
    expect(screen.getByText(/メールアドレスを入力してください/i)).toBeInTheDocument()
    fireEvent.change(screen.getByPlaceholderText('メールアドレスを入力'), {
      target: { value: 'alice@example.com' }
    })
    expect(screen.queryByText(/メールアドレスを入力してください/i)).not.toBeInTheDocument()
  })
})

Output:

TEXT 📖 参照専用
Renders the ▶ サンプル: フォームバリデーションテスト component UI as described in the section.


6. Server Actions のテスト(Prisma のモック + revalidatePath)

Server Actions ではデータベース操作とキャッシュ無効化関数をシミュレートする必要があります。

100%
graph TB
    A[Server Action のテスト] --> B[Prisma Client のモック]
    A --> C[revalidatePath のモック]
    A --> D[redirect のモック]
    B --> E[シミュレートデータを返却]
    C --> F[呼び出し回数/パラメータを検証]
    D --> G[リダイレクトパスを検証]
    
    style A fill:#cce5ff
    style B fill:#d4edda
    style C fill:#fff3cd

(1) Prisma モックツール

TS
// src/__tests__/utils/mock-prisma.ts
import { vi } from 'vitest'

export function createMockPrisma() {
  return {
    task: {
      findMany: vi.fn().mockResolvedValue([
        { id: '1', title: 'テストタスク', status: 'TODO', projectId: 'p1' }
      ]),
      create: vi.fn().mockImplementation(({ data }) => Promise.resolve({
        id: 'new-id',
        ...data,
        createdAt: new Date()
      })),
      update: vi.fn().mockImplementation(({ data }) => Promise.resolve(data)),
      delete: vi.fn().mockResolvedValue({ id: 'deleted-id' })
    },
    project: {
      findUnique: vi.fn().mockResolvedValue({
        id: 'p1',
        name: 'テストプロジェクト',
        tasks: []
      })
    },
    $transaction: vi.fn().mockImplementation((cb) => cb(createMockPrisma()))
  } as any
}

(2) Server Action テストの例

TS
// src/actions/task.ts
'use server'

import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'

const taskSchema = z.object({
  title: z.string().min(1, 'タイトルは必須です。'),
  projectId: z.string().min(1),
  status: z.enum(['TODO', 'IN_PROGRESS', 'DONE'])
})

export async function createTask(formData: FormData) {
  const data = Object.fromEntries(formData)
  const parsed = taskSchema.safeParse(data)
  
  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors }
  }
  
  await prisma.task.create({ data: parsed.data })
  revalidatePath(`/projects/${parsed.data.projectId}`)
  redirect(`/projects/${parsed.data.projectId}`)
}

▶ サンプル: 完全な Server Action テスト

Output:

TEXT 📖 参照専用
Validates input with Zod schema before processing.
Writes to the database and refreshes the affected page cache.
Returns error object if validation or processing fails.
TS
// src/__tests__/actions/task.test.ts
import { createTask } from '@/actions/task'
import { createMockPrisma } from '../utils/mock-prisma'
import { vi, describe, it, expect, beforeEach } from 'vitest'

vi.mock('@/lib/prisma', () => ({ prisma: createMockPrisma() }))
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
vi.mock('next/navigation', () => ({ redirect: vi.fn() }))

describe('createTask', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })
  
  it('タスクを正常に作成する', async () => {
    const formData = new FormData()
    formData.append('title', 'テストドキュメント作成')
    formData.append('projectId', 'p1')
    formData.append('status', 'TODO')
    
    const result = await createTask(formData)
    expect(result).toBeUndefined()
  })
  
  it('空のタイトルでバリデーションエラーを返す', async () => {
    const formData = new FormData()
    formData.append('title', '')
    formData.append('projectId', 'p1')
    formData.append('status', 'TODO')
    
    const result = await createTask(formData)
    expect(result).toHaveProperty('error')
    expect(result.error.title).toBeDefined()
  })
  
  it('作成後に revalidatePath を呼び出す', async () => {
    const formData = new FormData()
    formData.append('title', '新しいタスク')
    formData.append('projectId', 'p1')
    formData.append('status', 'TODO')
    
    await createTask(formData)
    const { revalidatePath } = await import('next/cache')
    expect(revalidatePath).toHaveBeenCalledWith('/projects/p1')
  })
})

Output:

TEXT 📖 参照専用
Creates a new database record.


7. MSW によるサーバーサイド API のモック

MSW (Mock Service Worker) はネットワークリクエストを傍受し、実際のバックエンドを起動せずにデータ取得ロジックをテストできます。

100%
graph LR
    A[コンポーネントが fetch を開始] --> B[MSW Service Worker]
    B --> C{リクエストマッチ}
    C -->|ハンドラに一致| D[モックデータを返却]
    C -->|不一致| E[実際のネットワークにパススルー]
    
    style B fill:#cce5ff
    style D fill:#d4edda
概念 説明
ハンドラ リクエストハンドラ http.get('/api/products', resolver)
リゾルバ モックレスポンスを返却 return HttpResponse.json([...])
サーバー Node.js モックサーバー setupServer(...handlers)
ブラウザ ブラウザ上の MSW setupWorker(...handlers)

(1) モックハンドラの定義

TS
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

const API_BASE = 'https://api.example.com'

export const handlers = [
  // GET /api/products
  http.get(`${API_BASE}/products`, () => {
    return HttpResponse.json([
      { id: 1, name: 'iPhone 16', price: 999, category: '電子機器' },
      { id: 2, name: 'Samsung S26', price: 899, category: '電子機器' }
    ])
  }),
  
  // POST /api/orders
  http.post(`${API_BASE}/orders`, async ({ request }) => {
    const body = await request.json()
    return HttpResponse.json({
      id: 'order-123',
      ...body as any,
      status: 'confirmed',
      createdAt: new Date().toISOString()
    })
  }),
  
  // GET /api/users/:id
  http.get(`${API_BASE}/users/:id`, ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: 'Alice Wang',
      email: 'alice@example.com',
      role: 'admin'
    })
  }),
  
  // エラーバウンダリテスト用の 500 エラー
  http.get(`${API_BASE}/errors/internal`, () => {
    return HttpResponse.json(
      { error: '内部サーバーエラー' },
      { status: 500 }
    )
  })
]

(2) テストセットアップへの統合

TS
// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)
TS
// src/__tests__/setup-test.ts(完全版)
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach, afterAll, beforeAll, vi } from 'vitest'
import { server } from '@/mocks/server'

beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }))
afterEach(() => { cleanup(); server.resetHandlers() })
afterAll(() => server.close())

// next/navigation のモック
vi.mock('next/navigation', () => ({
  useRouter: () => ({
    push: vi.fn(), replace: vi.fn(),
    refresh: vi.fn(), back: vi.fn(), forward: vi.fn()
  }),
  usePathname: () => '/',
  useSearchParams: () => new URLSearchParams()
}))

▶ サンプル: MSW 傍受 API のテスト

Output:

TEXT 📖 参照専用
TypeScript module executes successfully.
TSX
// src/__tests__/api-integration.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { http, HttpResponse } from 'msw'
import { server } from '@/mocks/server'
import ProductsPage from '@/app/products/page'

describe('ProductsPage with MSW', () => {
  it('モック API から製品をレンダリングする', async () => {
    const page = await ProductsPage()
    render(page)
    
    expect(screen.getByText('iPhone 16')).toBeInTheDocument()
    expect(screen.getByText('Samsung S26')).toBeInTheDocument()
  })
  
  it('空の製品リストを処理する', async () => {
    server.use(
      http.get('https://api.example.com/products', () => {
        return HttpResponse.json([])
      })
    )
    
    const page = await ProductsPage()
    render(page)
    await waitFor(() => {
      expect(screen.queryByTestId('product-item')).not.toBeInTheDocument()
    })
  })
  
  it('API 障害時にエラーを表示する', async () => {
    server.use(
      http.get('https://api.example.com/products', () => {
        return HttpResponse.json(
          { error: 'サービス利用不可' },
          { status: 503 }
        )
      })
    )
    
    await expect(ProductsPage()).rejects.toThrow()
  })
})

Output:

TEXT 📖 参照専用
Renders the ▶ サンプル: MSW 傍受 API のテスト component UI as described in the section.


8. 完全なサンプル: Todo アプリのフルスタックテスト

TSX
// src/__tests__/todo-comprehensive.test.ts
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { http, HttpResponse } from 'msw'
import { server } from '@/mocks/server'
import { createMockPrisma } from './utils/mock-prisma'
import { describe, it, expect, vi, beforeEach } from 'vitest'

// ============================================
// 総合テスト: Todo アプリケーション コンポーネント + Action + API
// ============================================

// --- 1. 依存のモック ---
vi.mock('@/lib/prisma', () => ({ prisma: createMockPrisma() }))
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
vi.mock('next/navigation', () => ({
  redirect: vi.fn(),
  useRouter: () => ({ push: vi.fn(), refresh: vi.fn() })
}))

// --- 2. Todo コンポーネント ---
function TodoItem({ id, title, done, onToggle }: {
  id: string; title: string; done: boolean
  onToggle: (id: string) => void
}) {
  return (
    <div data-testid="todo-item">
      <span style={{ textDecoration: done ? 'line-through' : 'none' }}>
        {title}
      </span>
      <button onClick={() => onToggle(id)}>
        {done ? '元に戻す' : '完了'}
      </button>
    </div>
  )
}

function TodoList({ todos }: { todos: Array<{ id: string; title: string; done: boolean }> }) {
  return (
    <div>
      {todos.map(t => (
        <TodoItem key={t.id} {...t} onToggle={(id) => {
          const idx = todos.findIndex(t => t.id === id)
          todos[idx].done = !todos[idx].done
        }} />
      ))}
    </div>
  )
}

// --- 3. テストケース ---
describe('Todo App', () => {
  const mockTodos = [
    { id: '1', title: 'Next.js テストを学ぶ', done: false },
    { id: '2', title: 'ユニットテストを書く', done: true },
    { id: '3', title: 'CI パイプラインを設定する', done: false }
  ]
  
  it('すべての Todo をレンダリングする', () => {
    render(<TodoList todos={mockTodos} />)
    expect(screen.getAllByTestId('todo-item')).toHaveLength(3)
    expect(screen.getByText('Next.js テストを学ぶ')).toBeInTheDocument()
  })
  
  it('完了アイテムに取り消し線を表示する', () => {
    render(<TodoList todos={mockTodos} />)
    const doneItem = screen.getByText('ユニットテストを書く')
    expect(doneItem).toHaveStyle('text-decoration: line-through')
  })
  
  it('ボタンクリックで Todo を切り替える', () => {
    render(<TodoList todos={mockTodos} />)
    const buttons = screen.getAllByRole('button')
    fireEvent.click(buttons[0])
    expect(buttons[0]).toHaveTextContent('元に戻す')
  })
  
  it('正しいボタンラベルを持つ', () => {
    render(<TodoList todos={mockTodos} />)
    const buttons = screen.getAllByRole('button')
    expect(buttons[0]).toHaveTextContent('完了')
    expect(buttons[1]).toHaveTextContent('元に戻す')
  })
})

// --- 4. MSW API テスト ---
describe('Todo API Mock', () => {
  it('モック API から Todo を取得する', async () => {
    server.use(
      http.get('https://api.example.com/todos', () => {
        return HttpResponse.json(mockTodos)
      })
    )
    
    const res = await fetch('https://api.example.com/todos')
    const data = await res.json()
    expect(data).toHaveLength(3)
    expect(data[0].title).toBe('Next.js テストを学ぶ')
  })
  
  it('API エラーを適切に処理する', async () => {
    server.use(
      http.get('https://api.example.com/todos', () => {
        return HttpResponse.json(null, { status: 500 })
      })
    )
    
    const res = await fetch('https://api.example.com/todos')
    expect(res.status).toBe(500)
  })
})
💻 出力:

TEXT 📖 参照専用
 ✓ src/__tests__/todo-comprehensive.test.ts (7 tests) 45ms

 Test Files  1 passed (1)
      Tests  7 passed (7)

❓ よくある質問

Q Vitest と Jest の違いは何ですか?
A Vitest は Vite と設定とプラグインエコシステムを共有し、Jest より 10 ~ 20 倍速く起動します(ネイティブ ESM サポート付き)。構文 API は Jest と互換性があります(globals API は expect/describe/it をサポート)。Next.js 16 は内部的に Turbopack(Vite エコシステムの一部)を使用しているため、Vitest がより自然な選択です。
Q cookies()headers() を使用するサーバーコンポーネントをテストするにはどうすればよいですか?
A これらの関数は Next.js のリクエストコンテキストに依存しており、テストではモックする必要があります。データ取得ロジックを別の Server Action または API レイヤーに抽出し、コンポーネントのレンダリングとビジネスロジックを分離してテストすることをお勧めします。
Q MSW と手動の fetch モックの違いは何ですか?
A MSW は Service Worker レベルでネットワークリクエストを傍受するため、コンポーネントコードを変更せずに実際の fetch 呼び出しチェーンをテストできます。global.fetch の手動モックは簡単ですが、複雑なリクエストマッチング、レスポンスのシリアル化、エラーシナリオを処理できません。
Q テストカバレッジの適切なしきい値はどのくらいですか?
A 段階的なアプローチをお勧めします: フェーズ 1 は 50%(コアコンポーネント + Server Actions)、フェーズ 2 は 70%(ブランチとエッジケースのカバー)、本番プロジェクトでは 80% 以上を目指します。カバレッジが最終目標ではないことを忘れないでください。クリティカルパス(決済/ログイン/データ書き込み)は 100% のカバレッジが必要です。
Q useActionState を使用するフォームをテストするにはどうすればよいですか?
A useActionState は React 19 の useFormState に依存しているため、テストでは react-domuseFormState をモックする必要があります。fireEvent.submit を使用してフォーム送信をトリガーし、送信後の UI 状態(成功/エラーメッセージ)をアサーションすることをお勧めします。
Q Testing Library の fireEventuserEvent の違いは何ですか?
A fireEvent は DOM イベント(クリックなど)を直接トリガーし、userEvent はより現実的なユーザー操作(キーボード入力やフォーカス管理など)をシミュレートします。本番テストでは userEvent(ユーザー体験により近い)を、ユニットテストでは fireEvent(より軽量)を使用することをお勧めします。

📖 まとめ


📝 練習問題

  1. 基本問題 (⭐): React プラグイン、jsdom 環境、@/ のパスエイリアスを含む vitest.config.ts を作成し、最もシンプルな render(

    こんにちは
    ) テストを記述して、jest-dom マッチャーが正常に動作することを検証してください。

  2. 応用問題 (⭐⭐): プロジェクトの Server Actions(createUsersubmitOrder など)の包括的なテストを記述してください: Prisma の create メソッドをモックし、revalidatePath が呼び出されることを検証し、Zod バリデーション失敗時のエラーレスポンスをテストします。

  3. 発展問題 (⭐⭐⭐): MSW を使用して 3 つのモックハンドラ(GET /api/tasks、POST /api/tasks、DELETE /api/tasks/:id)を構築し、TaskList コンポーネントの少なくとも 8 つのテストケース(データ取得、作成、削除機能を含み、初期読み込み、空リスト、エラー処理を含む)を記述してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%