# 2026年のReactテスト: Vitest、React Testing Libraryとベストプラクティス
> 2026年最新のReactテスト手法を徹底解説。Vitest、React Testing Libraryの活用法、コンポーネントテスト、統合テスト、技術面接対策まで網羅的に紹介します。
- Published: 2026-07-09
- Updated: 2026-07-09
- Author: SharpSkill
- Tags: react, testing, vitest, react-testing-library, frontend
- Reading time: 12 min
---
2026年現在、Reactアプリケーションのテストは開発プロセスにおいて不可欠な要素となっています。Vitestの登場により、高速で設定が簡単なテスト環境が実現し、React Testing Library(RTL)との組み合わせによってユーザー視点でのテストが標準的な手法として確立されました。本記事では、最新のReactテスト技術、ベストプラクティス、そして技術面接でよく問われる質問について詳しく解説します。
> **なぜVitestなのか**
>
> Vitestは、Viteベースの高速テストフレームワークです。Jestと互換性のあるAPIを提供しながら、ESモジュールネイティブサポート、ホットモジュールリプレースメント(HMR)によるウォッチモードの高速化、TypeScriptの設定不要なサポートなど、現代的な開発体験を提供します。2026年のReactプロジェクトでは、Vitestが事実上の標準テストフレームワークとなっています。
## Vitestの環境構築とReact Testing Libraryの統合
VitestとReact Testing Libraryを組み合わせることで、高速かつ信頼性の高いテスト環境を構築できます。まず、必要なパッケージをインストールします。
```bash
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
```
Vitestの設定ファイルでは、React環境とグローバルなテストユーティリティを設定します。
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
include: ['**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'src/test/'],
},
},
})
```
セットアップファイルでは、`jest-dom`のマッチャーを拡張し、テスト間のクリーンアップを設定します。
```typescript
// src/test/setup.ts
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach, vi } from 'vitest'
afterEach(() => {
cleanup()
})
// Mock window.matchMedia for responsive components
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
```
この設定により、各テストファイルで`describe`、`it`、`expect`などのグローバル関数を使用でき、各テスト後に自動的にDOMがクリーンアップされます。
## コンポーネントテストの基本パターン
React Testing Libraryの核心的な考え方は、「ユーザーがコンポーネントを使用する方法でテストする」ことです。実装の詳細ではなく、ユーザーの視点からコンポーネントの動作を検証します。
基本的なコンポーネントテストの例を見てみましょう。
```typescript
// src/components/Button.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
import { Button } from './Button'
describe('Button', () => {
it('renders with correct text', () => {
render()
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument()
})
it('calls onClick handler when clicked', async () => {
const user = userEvent.setup()
const handleClick = vi.fn()
render()
await user.click(screen.getByRole('button', { name: /submit/i }))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('is disabled when loading', () => {
render()
expect(screen.getByRole('button')).toBeDisabled()
})
it('shows spinner when loading', () => {
render()
expect(screen.getByRole('button')).toContainElement(
screen.getByTestId('spinner')
)
})
})
```
`screen.getByRole`を使用することで、アクセシビリティの観点からも正しいマークアップを促進します。`userEvent`はブラウザでの実際のユーザー操作をより正確にシミュレートするため、`fireEvent`よりも推奨されます。
## 非同期処理とデータフェッチのテスト
Reactアプリケーションでは、APIからのデータフェッチなど非同期処理が頻繁に発生します。これらのテストには、`waitFor`や`findBy`クエリを使用します。
```typescript
// src/components/UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { UserProfile } from './UserProfile'
// Mock the fetch API
const mockUser = {
id: '1',
name: 'John Doe',
email: 'john@example.com',
}
beforeEach(() => {
vi.spyOn(global, 'fetch').mockResolvedValue({
ok: true,
json: async () => mockUser,
} as Response)
})
describe('UserProfile', () => {
it('displays loading state initially', () => {
render()
expect(screen.getByText(/loading/i)).toBeInTheDocument()
})
it('displays user data after fetch', async () => {
render()
// Wait for the user name to appear
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument()
})
expect(screen.getByText('john@example.com')).toBeInTheDocument()
})
it('displays error message on fetch failure', async () => {
vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'))
render()
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent(/error/i)
})
})
})
```
`findBy`クエリは内部的に`waitFor`を使用するため、非同期要素の待機に便利です。
```typescript
it('displays user data after fetch', async () => {
render()
// findBy automatically waits for the element
const userName = await screen.findByText('John Doe')
expect(userName).toBeInTheDocument()
})
```
## フォームテストとユーザーインタラクション
フォームのテストは、バリデーション、送信、エラー表示など複数の側面を検証する必要があります。
```typescript
// src/components/LoginForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
import { LoginForm } from './LoginForm'
describe('LoginForm', () => {
it('submits form with valid data', async () => {
const user = userEvent.setup()
const handleSubmit = vi.fn()
render()
await user.type(screen.getByLabelText(/email/i), 'test@example.com')
await user.type(screen.getByLabelText(/password/i), 'password123')
await user.click(screen.getByRole('button', { name: /sign in/i }))
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
})
})
})
it('displays validation errors for invalid email', async () => {
const user = userEvent.setup()
render()
await user.type(screen.getByLabelText(/email/i), 'invalid-email')
await user.click(screen.getByRole('button', { name: /sign in/i }))
await waitFor(() => {
expect(screen.getByText(/valid email/i)).toBeInTheDocument()
})
})
it('disables submit button while submitting', async () => {
const user = userEvent.setup()
const handleSubmit = vi.fn(() => new Promise((r) => setTimeout(r, 100)))
render()
await user.type(screen.getByLabelText(/email/i), 'test@example.com')
await user.type(screen.getByLabelText(/password/i), 'password123')
await user.click(screen.getByRole('button', { name: /sign in/i }))
expect(screen.getByRole('button', { name: /sign in/i })).toBeDisabled()
})
})
```
`userEvent.type`は実際のキーボード入力をシミュレートし、各キーストロークでイベントを発火させます。これにより、入力中のバリデーションやデバウンス処理も正しくテストできます。
## カスタムフックのテスト
カスタムフックのテストには、`@testing-library/react`の`renderHook`を使用します。
```typescript
// src/hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('initializes with default value', () => {
const { result } = renderHook(() => useCounter())
expect(result.current.count).toBe(0)
})
it('initializes with provided value', () => {
const { result } = renderHook(() => useCounter(10))
expect(result.current.count).toBe(10)
})
it('increments counter', () => {
const { result } = renderHook(() => useCounter())
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
it('decrements counter', () => {
const { result } = renderHook(() => useCounter(5))
act(() => {
result.current.decrement()
})
expect(result.current.count).toBe(4)
})
it('resets counter to initial value', () => {
const { result } = renderHook(() => useCounter(10))
act(() => {
result.current.increment()
result.current.increment()
result.current.reset()
})
expect(result.current.count).toBe(10)
})
})
```
状態を変更する操作は必ず`act`でラップします。これにより、Reactの状態更新が完了してからアサーションが実行されます。
## コンテキストとプロバイダーのテスト
コンテキストを使用するコンポーネントをテストするには、テスト用のラッパーを作成します。
```typescript
// src/test/utils.tsx
import { ReactNode } from 'react'
import { render, RenderOptions } from '@testing-library/react'
import { ThemeProvider } from '@/contexts/ThemeContext'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
})
interface WrapperProps {
children: ReactNode
}
function AllProviders({ children }: WrapperProps) {
const queryClient = createTestQueryClient()
return (
{children}
)
}
const customRender = (
ui: React.ReactElement,
options?: Omit
) => render(ui, { wrapper: AllProviders, ...options })
export * from '@testing-library/react'
export { customRender as render }
```
このカスタム`render`関数を使用することで、すべてのテストで一貫したプロバイダー構成を維持できます。
## モックとスパイの効果的な活用
Vitestでは、`vi.mock`と`vi.spyOn`を使用してモジュールや関数をモック化できます。
```typescript
// src/services/api.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { fetchUsers, createUser } from './api'
// Mock the entire module
vi.mock('./http-client', () => ({
httpClient: {
get: vi.fn(),
post: vi.fn(),
},
}))
import { httpClient } from './http-client'
describe('API Service', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches users successfully', async () => {
const mockUsers = [{ id: '1', name: 'John' }]
vi.mocked(httpClient.get).mockResolvedValue({ data: mockUsers })
const users = await fetchUsers()
expect(httpClient.get).toHaveBeenCalledWith('/users')
expect(users).toEqual(mockUsers)
})
it('creates user with correct payload', async () => {
const newUser = { name: 'Jane', email: 'jane@example.com' }
vi.mocked(httpClient.post).mockResolvedValue({ data: { id: '2', ...newUser } })
await createUser(newUser)
expect(httpClient.post).toHaveBeenCalledWith('/users', newUser)
})
})
```
`vi.mocked`は型安全なモックを提供し、TypeScriptの補完機能を活用できます。
## React Testing面接でよくある質問
技術面接では、Reactテストに関する深い理解が求められます。以下は、よく出題される質問とその回答です。
**Q1: `getBy`、`queryBy`、`findBy`の違いを説明してください。**
`getBy`は要素が存在することを前提とし、見つからない場合はエラーをスローします。同期的に動作し、要素が確実に存在する場合に使用します。`queryBy`は要素が見つからない場合に`null`を返します。要素が存在しないことをアサートする場合に使用します。`findBy`は非同期で動作し、要素が現れるまで待機します。デフォルトで1000msのタイムアウトがあり、非同期レンダリングやデータフェッチ後の要素に使用します。
**Q2: なぜ`fireEvent`より`userEvent`が推奨されるのですか。**
`userEvent`は実際のブラウザでのユーザー操作をより正確にシミュレートします。例えば、`userEvent.type`はキーボードイベント(keydown、keypress、keyup)と入力イベントの両方を発火させ、フォーカス管理も行います。一方、`fireEvent`は単一のイベントのみを発火させるため、実際のユーザー操作と異なる動作になる可能性があります。`userEvent`を使用することで、より現実的なテストシナリオを作成できます。
**Q3: スナップショットテストの利点と欠点は何ですか。**
利点として、UIの予期しない変更を検出でき、設定が簡単で大きなコンポーネントツリーも素早くテストできます。欠点として、頻繁に変更されるコンポーネントではメンテナンスコストが高く、実際の動作ではなく出力構造のみをテストするため、重要なバグを見逃す可能性があります。また、開発者がスナップショットを無批判に更新してしまう傾向があります。ベストプラクティスとして、スナップショットテストは静的なUIコンポーネントに限定し、インタラクティブな機能はユーザーイベントベースのテストで検証することが推奨されます。
**Q4: テストカバレッジ100%を目指すべきですか。**
100%のカバレッジは必ずしも高品質なテストを意味しません。重要なのは、ビジネスクリティカルなコードパスが十分にテストされていることです。カバレッジは指標の一つに過ぎず、テストの品質や意味のあるアサーションの存在を保証しません。一般的に、80%程度のカバレッジを目標とし、残りの20%は複雑性やリスクに基づいて判断します。ユーティリティ関数やビジネスロジックには高いカバレッジを維持し、UIコンポーネントはユーザーインタラクションに焦点を当てたテストを優先することが効果的です。
**Q5: テストピラミッドとテストトロフィーの違いは何ですか。**
テストピラミッドは、ユニットテストを多く、統合テストを中程度、E2Eテストを少なく配置する従来のアプローチです。テストトロフィーは、Reactアプリケーションに適した現代的なアプローチで、統合テストを最も重視します。これは、コンポーネント間の相互作用をテストすることで、ユニットテストよりも高い信頼性を得られるためです。React Testing Libraryの作者であるKent C. Doddsが提唱し、「テストがソフトウェアの使用方法に似ているほど、より多くの信頼を得られる」という原則に基づいています。
## 統合テストのベストプラクティス
統合テストでは、複数のコンポーネントが連携して動作することを検証します。
```typescript
// src/features/checkout/Checkout.integration.test.tsx
import { render, screen, waitFor } from '@/test/utils'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
import { CheckoutPage } from './CheckoutPage'
import { server } from '@/test/mocks/server'
import { http, HttpResponse } from 'msw'
describe('Checkout Flow', () => {
it('completes checkout successfully', async () => {
const user = userEvent.setup()
render()
// Fill shipping information
await user.type(screen.getByLabelText(/full name/i), 'John Doe')
await user.type(screen.getByLabelText(/address/i), '123 Main St')
await user.type(screen.getByLabelText(/city/i), 'New York')
await user.click(screen.getByRole('button', { name: /continue to payment/i }))
// Fill payment information
await user.type(screen.getByLabelText(/card number/i), '4242424242424242')
await user.type(screen.getByLabelText(/expiry/i), '12/28')
await user.type(screen.getByLabelText(/cvc/i), '123')
await user.click(screen.getByRole('button', { name: /place order/i }))
// Verify success state
await waitFor(() => {
expect(screen.getByText(/order confirmed/i)).toBeInTheDocument()
})
})
it('handles payment failure gracefully', async () => {
// Override the default handler for this test
server.use(
http.post('/api/payments', () => {
return HttpResponse.json(
{ error: 'Payment declined' },
{ status: 400 }
)
})
)
const user = userEvent.setup()
render()
// ... fill form and submit ...
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent(/payment declined/i)
})
})
})
```
MSW(Mock Service Worker)を使用することで、ネットワークレベルでAPIをモック化し、より現実的な統合テストを実現できます。
## テストのパフォーマンス最適化
大規模なテストスイートでは、実行時間の最適化が重要です。
```typescript
// vitest.config.ts
export default defineConfig({
test: {
// Run tests in parallel
pool: 'threads',
poolOptions: {
threads: {
singleThread: false,
},
},
// Only run affected tests in watch mode
watch: {
include: ['src/**/*.{ts,tsx}'],
},
// Isolate test files for reliability
isolate: true,
},
})
```
また、重いセットアップは`beforeAll`で一度だけ実行し、`beforeEach`での再初期化を最小限に抑えます。
## まとめ: 信頼性の高いReactテストを書くために
2026年のReactテストにおいて、VitestとReact Testing Libraryの組み合わせは、高速で信頼性の高いテスト環境を提供します。
本記事で学んだ主要なポイント:
- **ユーザー視点でテストする**: 実装の詳細ではなく、ユーザーがコンポーネントをどのように使用するかに焦点を当てる
- **適切なクエリを選択する**: `getBy`、`queryBy`、`findBy`を状況に応じて使い分ける
- **`userEvent`を優先する**: より現実的なユーザーインタラクションをシミュレートする
- **統合テストを重視する**: テストトロフィーの考え方に基づき、コンポーネント間の連携をテストする
- **モックを適切に使用する**: 外部依存はモック化し、内部実装はできるだけ実際のコードを使用する
Reactテストのスキルは、コードの品質を保証するだけでなく、リファクタリングの安全性を高め、チーム全体の開発速度を向上させます。技術面接においても、テストに関する深い理解は、候補者の実践的な開発能力を示す重要な指標となります。
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/ja/blog/react-next/react-testing-2026-vitest-rtl-best-practices