Next.js: ユニットテスト & 統合テスト
最終更新:2026-08-26
テストはオプションではありません。本番レベルのアプリケーションの「エアバッグ」です。普段は必要ありませんが、いざという時に命を救います。
1. 学習目標
- Vitest を使用した Next.js 16 テスト環境のセットアップ(vitest.config.ts + React 互換レイヤー)
- @testing-library/react を使用したサーバーコンポーネントとクライアントコンポーネントのレンダリングとアサーション
- Server Actions の統合テストの作成(Prisma のモック + revalidatePath のシミュレーション)
- jest-dom カスタムマッチャーの使用(toBeInTheDocument / toHaveTextContent)
- MSW を使用した外部 API リクエストの傍受とネットワーク依存の分離
2. フルスタックエンジニアの実話
(1) 課題: ローンチ前夜、1 つのスペースで決済ページがクラッシュ
Alice は中東市場向けの EC プラットフォームでフルスタックエンジニアとして働いています。プラットフォームは 1 日 5 万件以上の注文を処理し、チームは週 2 回のリリースサイクルを維持しています。
先週金曜日のローンチで、一見無害な変更 — OrderSummary コンポーネントの価格フォーマット関数に余分なスペースを追加したこと — により、サウジアラビアのユーザーの支払い金額に小数点が 1 桁余分に表示されました。データ自体は実際には正しくありませんでしたが、カスタマーサービスに 200 件以上の苦情が寄せられ、3 人のユーザーが注文をキャンセルしました。
さらに悪いことに:
- チームはこのコンポーネントに対してテストを一切実施していませんでした。
- 手動テストは Chrome で 2 回クリックしただけで承認されました。
- このバグはコードレビューでも検出されませんでした。
Alice は決意しました: このような問題が再発しないように、テスト体制を確立しなければなりません。
(2) Vitest + Testing Library の解決策
Alice はプロジェクトに Vitest テストスタックを導入しました:
npm install -D vitest @testing-library/react @testing-library/jest-dom @vitejs/plugin-react msw
そして最初のテストを作成しました:
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 を使用)とネイティブに互換性があります。
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 の設定
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) グローバルセットアップファイル
// 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 の動作確認
// 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()
})
npx vitest run
✓ src/__tests__/basic.test.ts (1 test) 12ms
Test Files 1 passed (1)
Tests 1 passed (1)
Output:
✓ src/__tests__/basic.test.ts (1 test) 12ms
Test Files 1 passed (1)
Tests 1 passed (1)
▶ サンプル: package.json へのテストスクリプトの追加
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}
Output:
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" のクエリ
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 出力を直接アサーションできます:
// 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
// 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:
Renders the Component component UI.
// 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:
An interactive component with state management.
Visible text: カウント: {count}
// 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:
Renders the Component component UI.
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:
Renders the ▶ サンプル: フォームバリデーションテスト component UI as described in the section.
6. Server Actions のテスト(Prisma のモック + revalidatePath)
Server Actions ではデータベース操作とキャッシュ無効化関数をシミュレートする必要があります。
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 モックツール
// 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 テストの例
// 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:
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.
// 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:
Creates a new database record.
7. MSW によるサーバーサイド API のモック
MSW (Mock Service Worker) はネットワークリクエストを傍受し、実際のバックエンドを起動せずにデータ取得ロジックをテストできます。
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) モックハンドラの定義
// 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) テストセットアップへの統合
// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
// 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:
TypeScript module executes successfully.
// 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:
Renders the ▶ サンプル: MSW 傍受 API のテスト component UI as described in the section.
8. 完全なサンプル: Todo アプリのフルスタックテスト
// 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)
})
})
✓ src/__tests__/todo-comprehensive.test.ts (7 tests) 45ms
Test Files 1 passed (1)
Tests 7 passed (7)
❓ よくある質問
expect/describe/it をサポート)。Next.js 16 は内部的に Turbopack(Vite エコシステムの一部)を使用しているため、Vitest がより自然な選択です。cookies() や headers() を使用するサーバーコンポーネントをテストするにはどうすればよいですか?global.fetch の手動モックは簡単ですが、複雑なリクエストマッチング、レスポンスのシリアル化、エラーシナリオを処理できません。useActionState を使用するフォームをテストするにはどうすればよいですか?useActionState は React 19 の useFormState に依存しているため、テストでは react-dom の useFormState をモックする必要があります。fireEvent.submit を使用してフォーム送信をトリガーし、送信後の UI 状態(成功/エラーメッセージ)をアサーションすることをお勧めします。fireEvent と userEvent の違いは何ですか?fireEvent は DOM イベント(クリックなど)を直接トリガーし、userEvent はより現実的なユーザー操作(キーボード入力やフォーカス管理など)をシミュレートします。本番テストでは userEvent(ユーザー体験により近い)を、ユニットテストでは fireEvent(より軽量)を使用することをお勧めします。📖 まとめ
- Vitest は Next.js 16 の推奨テストフレームワークです。
vitest.config.tsの設定時にenvironment: 'jsdom'と React プラグインを指定する必要があります。 @testing-library/reactはrenderとscreenAPI を提供し、実装の詳細ではなくユーザーに見える動作に焦点を当てます- jest-dom マッチャー(
toBeInTheDocument、toHaveTextContent、toBeVisible)により、テストアサーションがより意味のあるものになります - Server Actions のテストでは、Prisma Client、
revalidatePath、redirectをモックしてデータ書き込みとキャッシュ無効化を検証する必要があります - MSW は Service Worker レイヤーでネットワークリクエストを傍受し、統合テストで外部 API を分離するのに適しています
setup-test.tsはテストインフラの主要ファイルであり、モックとグローバル初期化を一元的に管理します
📝 練習問題
-
基本問題 (⭐): React プラグイン、jsdom 環境、
@/のパスエイリアスを含むvitest.config.tsを作成し、最もシンプルなrender(こんにちは)テストを記述して、jest-dom マッチャーが正常に動作することを検証してください。 -
応用問題 (⭐⭐): プロジェクトの Server Actions(
createUserやsubmitOrderなど)の包括的なテストを記述してください: Prisma のcreateメソッドをモックし、revalidatePathが呼び出されることを検証し、Zod バリデーション失敗時のエラーレスポンスをテストします。 -
発展問題 (⭐⭐⭐): MSW を使用して 3 つのモックハンドラ(GET /api/tasks、POST /api/tasks、DELETE /api/tasks/:id)を構築し、TaskList コンポーネントの少なくとも 8 つのテストケース(データ取得、作成、削除機能を含み、初期読み込み、空リスト、エラー処理を含む)を記述してください。