なぜVitestがJestより高速なのか
Vitestは従来のJestと比較して圧倒的に高速です。その理由は主に3つあります。
まず、VitestはViteのビルドパイプラインを活用しています。Viteは開発サーバーでESモジュールをそのまま利用するため、トランスパイルのオーバーヘッドが大幅に削減されます。
// Jest(従来の方法)
// すべてのファイルをトランスパイルしてから実行
// 起動時間: 約3-5秒
// Vitest(高速な方法)
// ESモジュールをそのまま実行
// 起動時間: 約0.1-0.3秒次に、Vitestはワーカースレッドを効率的に使用しています。複数のテストファイルを並列実行することで、マルチコアCPUの性能を最大限に活用できます。
最後に、Vitestはインテリジェントなファイル監視機能を持っています。変更されたファイルに関連するテストのみを再実行するため、開発中の待ち時間が劇的に短縮されます。
Vitestの初期セットアップと基本設定
Vitestのセットアップは驚くほど簡単です。以下の手順で始められます。
# Vitestと必要な依存関係をインストール
npm install -D vitest @testing-library/react @testing-library/jest-dom次に、vite.config.tsにテスト設定を追加します。
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
css: true,
},
})セットアップファイルでは、Testing Libraryのカスタムマッチャーを設定します。
// src/test/setup.ts
import '@testing-library/jest-dom'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'
// 各テスト後に自動的にクリーンアップ
afterEach(() => {
cleanup()
})Testing Libraryとの組み合わせ方
VitestとTesting Libraryは相性抜群です。Testing Libraryの「ユーザー視点でテストを書く」という理念により、メンテナンスしやすいテストが書けます。
基本的なコンポーネントテストの例を見てみましょう。
// Button.tsx
interface ButtonProps {
onClick: () => void
children: React.ReactNode
disabled?: boolean
}
export const Button = ({ onClick, children, disabled }: ButtonProps) => {
return (
<button onClick={onClick} disabled={disabled}>
{children}
</button>
)
}// Button.test.tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'
describe('Button', () => {
it('クリックイベントが正しく動作する', () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick}>クリック</Button>)
const button = screen.getByText('クリック')
fireEvent.click(button)
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('無効状態でクリックできない', () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick} disabled>無効</Button>)
const button = screen.getByText('無効')
fireEvent.click(button)
expect(handleClick).not.toHaveBeenCalled()
expect(button).toBeDisabled()
})
})コンポーネントテストの実践パターン
実際の開発でよく使うテストパターンを紹介します。フォームコンポーネントのテストを例に見てみましょう。
// LoginForm.tsx
import { useState } from 'react'
interface LoginFormProps {
onSubmit: (data: { email: string; password: string }) => void
}
export const LoginForm = ({ onSubmit }: LoginFormProps) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [errors, setErrors] = useState<Record<string, string>>({})
const validate = () => {
const newErrors: Record<string, string> = {}
if (!email) newErrors.email = 'メールアドレスは必須です'
if (!password) newErrors.password = 'パスワードは必須です'
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (validate()) {
onSubmit({ email, password })
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="メールアドレス"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
{errors.email && <span role="alert">{errors.email}</span>}
<input
type="password"
placeholder="パスワード"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{errors.password && <span role="alert">{errors.password}</span>}
<button type="submit">ログイン</button>
</form>
)
}このコンポーネントのテストは以下のように書けます。
// LoginForm.test.tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen, userEvent } from '@testing-library/react'
import { LoginForm } from './LoginForm'
describe('LoginForm', () => {
it('正しい入力でフォームが送信される', async () => {
const handleSubmit = vi.fn()
const user = userEvent.setup()
render(<LoginForm onSubmit={handleSubmit} />)
await user.type(screen.getByPlaceholderText('メールアドレス'), 'test@example.com')
await user.type(screen.getByPlaceholderText('パスワード'), 'password123')
await user.click(screen.getByText('ログイン'))
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
})
})
it('空の入力でエラーが表示される', async () => {
const handleSubmit = vi.fn()
const user = userEvent.setup()
render(<LoginForm onSubmit={handleSubmit} />)
await user.click(screen.getByText('ログイン'))
expect(screen.getByText('メールアドレスは必須です')).toBeInTheDocument()
expect(screen.getByText('パスワードは必須です')).toBeInTheDocument()
expect(handleSubmit).not.toHaveBeenCalled()
})
})非同期処理とモックの効率的な書き方
非同期処理のテストはVitestの強力なモック機能で簡単に書けます。APIコールを含むコンポーネントのテストを見てみましょう。
// UserList.tsx
import { useState, useEffect } from 'react'
interface User {
id: number
name: string
email: string
}
export const UserList = () => {
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data)
setLoading(false)
})
.catch(err => {
setError('ユーザーの取得に失敗しました')
setLoading(false)
})
}, [])
if (loading) return <div>読み込み中...</div>
if (error) return <div role="alert">{error}</div>
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
)
}このコンポーネントのテストでは、fetchをモックします。
// UserList.test.tsx
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { UserList } from './UserList'
// fetchのモック
global.fetch = vi.fn()
describe('UserList', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('ユーザーリストが正しく表示される', async () => {
const mockUsers = [
{ id: 1, name: '田中太郎', email: 'tanaka@example.com' },
{ id: 2, name: '鈴木花子', email: 'suzuki@example.com' }
]
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUsers
})
render(<UserList />)
// 読み込み中の表示を確認
expect(screen.getByText('読み込み中...')).toBeInTheDocument()
// データ取得後の表示を確認
await waitFor(() => {
expect(screen.getByText('田中太郎 (tanaka@example.com)')).toBeInTheDocument()
expect(screen.getByText('鈴木花子 (suzuki@example.com)')).toBeInTheDocument()
})
})
it('エラー時にメッセージが表示される', async () => {
fetch.mockRejectedValueOnce(new Error('Network error'))
render(<UserList />)
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('ユーザーの取得に失敗しました')
})
})
})モックの便利な使い方として、vi.mockを使ったモジュール全体のモックもあります。
// API呼び出しモジュールのモック
vi.mock('./api', () => ({
getUsers: vi.fn(() => Promise.resolve([
{ id: 1, name: 'テストユーザー', email: 'test@example.com' }
]))
}))パフォーマンスを最大化するテスト設計
Vitestで高速なテストを実現するには、いくつかのベストプラクティスがあります。
まず、テストの並列実行を活用します。vitest.config.tsで設定できます。
// vitest.config.ts
export default defineConfig({
test: {
// CPUコア数に応じて最適化
threads: true,
maxThreads: 4,
minThreads: 1,
}
})次に、重いテストは分離して実行します。
// heavy.test.ts
import { describe, it } from 'vitest'
describe('重いテスト', () => {
it.concurrent('並列実行可能なテスト1', async () => {
// 時間のかかる処理
})
it.concurrent('並列実行可能なテスト2', async () => {
// 時間のかかる処理
})
})テストの実行時間を計測して、ボトルネックを特定することも重要です。
# テストの実行時間を表示
vitest --reporter=verbose
# カバレッジレポートも同時に生成
vitest --coverage最後に、共通のセットアップ処理は効率化しましょう。
// test-utils.tsx
import { render } from '@testing-library/react'
import { ReactElement } from 'react'
// カスタムレンダー関数
export const renderWithProviders = (ui: ReactElement) => {
return render(
<ThemeProvider theme={theme}>
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>
</ThemeProvider>
)
}
// 使用例
import { renderWithProviders } from './test-utils'
it('プロバイダー付きのテスト', () => {
renderWithProviders(<MyComponent />)
// テストコード
})これらのテクニックを組み合わせることで、大規模なプロジェクトでも高速なテスト実行が可能になります。Vitestの並列実行とインテリジェントなキャッシュにより、開発効率が大幅に向上します。
最短で課題解決する一冊
この記事の内容と高い親和性が確認できたベストマッチです。早めにチェックしておきましょう。
