2026年のReactテスト: Vitest、React Testing Libraryとベストプラクティス
2026年最新のReactテスト手法を徹底解説。Vitest、React Testing Libraryの活用法、コンポーネントテスト、統合テスト、技術面接対策まで網羅的に紹介します。

2026年現在、Reactアプリケーションのテストは開発プロセスにおいて不可欠な要素となっています。Vitestの登場により、高速で設定が簡単なテスト環境が実現し、React Testing Library(RTL)との組み合わせによってユーザー視点でのテストが標準的な手法として確立されました。本記事では、最新のReactテスト技術、ベストプラクティス、そして技術面接でよく問われる質問について詳しく解説します。
Vitestは、Viteベースの高速テストフレームワークです。Jestと互換性のあるAPIを提供しながら、ESモジュールネイティブサポート、ホットモジュールリプレースメント(HMR)によるウォッチモードの高速化、TypeScriptの設定不要なサポートなど、現代的な開発体験を提供します。2026年のReactプロジェクトでは、Vitestが事実上の標準テストフレームワークとなっています。
Vitestの環境構築とReact Testing Libraryの統合
VitestとReact Testing Libraryを組み合わせることで、高速かつ信頼性の高いテスト環境を構築できます。まず、必要なパッケージをインストールします。
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdomVitestの設定ファイルでは、React環境とグローバルなテストユーティリティを設定します。
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のマッチャーを拡張し、テスト間のクリーンアップを設定します。
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の核心的な考え方は、「ユーザーがコンポーネントを使用する方法でテストする」ことです。実装の詳細ではなく、ユーザーの視点からコンポーネントの動作を検証します。
基本的なコンポーネントテストの例を見てみましょう。
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(<Button>Click me</Button>)
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(<Button onClick={handleClick}>Submit</Button>)
await user.click(screen.getByRole('button', { name: /submit/i }))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('is disabled when loading', () => {
render(<Button isLoading>Loading...</Button>)
expect(screen.getByRole('button')).toBeDisabled()
})
it('shows spinner when loading', () => {
render(<Button isLoading>Submit</Button>)
expect(screen.getByRole('button')).toContainElement(
screen.getByTestId('spinner')
)
})
})screen.getByRoleを使用することで、アクセシビリティの観点からも正しいマークアップを促進します。userEventはブラウザでの実際のユーザー操作をより正確にシミュレートするため、fireEventよりも推奨されます。
非同期処理とデータフェッチのテスト
Reactアプリケーションでは、APIからのデータフェッチなど非同期処理が頻繁に発生します。これらのテストには、waitForやfindByクエリを使用します。
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(<UserProfile userId="1" />)
expect(screen.getByText(/loading/i)).toBeInTheDocument()
})
it('displays user data after fetch', async () => {
render(<UserProfile userId="1" />)
// 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(<UserProfile userId="1" />)
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent(/error/i)
})
})
})findByクエリは内部的にwaitForを使用するため、非同期要素の待機に便利です。
it('displays user data after fetch', async () => {
render(<UserProfile userId="1" />)
// findBy automatically waits for the element
const userName = await screen.findByText('John Doe')
expect(userName).toBeInTheDocument()
})フォームテストとユーザーインタラクション
フォームのテストは、バリデーション、送信、エラー表示など複数の側面を検証する必要があります。
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(<LoginForm onSubmit={handleSubmit} />)
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(<LoginForm onSubmit={vi.fn()} />)
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(<LoginForm onSubmit={handleSubmit} />)
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は実際のキーボード入力をシミュレートし、各キーストロークでイベントを発火させます。これにより、入力中のバリデーションやデバウンス処理も正しくテストできます。
React / Next.jsの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
カスタムフックのテスト
カスタムフックのテストには、@testing-library/reactのrenderHookを使用します。
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の状態更新が完了してからアサーションが実行されます。
コンテキストとプロバイダーのテスト
コンテキストを使用するコンポーネントをテストするには、テスト用のラッパーを作成します。
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 (
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="light">{children}</ThemeProvider>
</QueryClientProvider>
)
}
const customRender = (
ui: React.ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => render(ui, { wrapper: AllProviders, ...options })
export * from '@testing-library/react'
export { customRender as render }このカスタムrender関数を使用することで、すべてのテストで一貫したプロバイダー構成を維持できます。
モックとスパイの効果的な活用
Vitestでは、vi.mockとvi.spyOnを使用してモジュールや関数をモック化できます。
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が提唱し、「テストがソフトウェアの使用方法に似ているほど、より多くの信頼を得られる」という原則に基づいています。
統合テストのベストプラクティス
統合テストでは、複数のコンポーネントが連携して動作することを検証します。
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(<CheckoutPage />)
// 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(<CheckoutPage />)
// ... fill form and submit ...
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent(/payment declined/i)
})
})
})MSW(Mock Service Worker)を使用することで、ネットワークレベルでAPIをモック化し、より現実的な統合テストを実現できます。
テストのパフォーマンス最適化
大規模なテストスイートでは、実行時間の最適化が重要です。
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テストのスキルは、コードの品質を保証するだけでなく、リファクタリングの安全性を高め、チーム全体の開発速度を向上させます。技術面接においても、テストに関する深い理解は、候補者の実践的な開発能力を示す重要な指標となります。
タグ
共有
関連記事

React Compiler 2026年版:自動メモ化の仕組みと面接対策の完全ガイド
React Compiler v1.0の自動メモ化の内部構造、コンパイルパイプライン、手動最適化が必要なケースを解説。2026年のReact技術面接で問われるポイントを網羅的にカバーします。

Next.js 16 の Server Actions【2026】ミューテーション・リバリデーション・面接質問
Next.js 16 の Server Actions がミューテーション、リバリデーション、pending 状態、楽観的 UI、セキュリティをどう扱うか、そして各概念を試す面接質問を解説します。

React 19 useEffectEventとActivity徹底解説: 新API・実装パターン・面接対策2026
React 19.2で導入されたuseEffectEventとActivityコンポーネントの動作原理、実装パターン、面接頻出問題を体系的に解説します。