Tasuke Hubのロゴ

ITを中心に困っている人を助けるメディア

分かりやすく解決策を提供することで、あなたの困ったをサポート。 全ての人々がスムーズに生活できる世界を目指します。

Vitestでフロントエンドテストが10倍速くなる理由とTesting Library実装例

記事のサムネイル

なぜ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('メールアドレス'), '[email protected]')
    await user.type(screen.getByPlaceholderText('パスワード'), 'password123')
    await user.click(screen.getByText('ログイン'))
    
    expect(handleSubmit).toHaveBeenCalledWith({
      email: '[email protected]',
      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: '[email protected]' },
      { id: 2, name: '鈴木花子', email: '[email protected]' }
    ]

    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockUsers
    })

    render(<UserList />)

    // 読み込み中の表示を確認
    expect(screen.getByText('読み込み中...')).toBeInTheDocument()

    // データ取得後の表示を確認
    await waitFor(() => {
      expect(screen.getByText('田中太郎 ([email protected])')).toBeInTheDocument()
      expect(screen.getByText('鈴木花子 ([email protected])')).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: '[email protected]' }
  ]))
}))

パフォーマンスを最大化するテスト設計

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の並列実行とインテリジェントなキャッシュにより、開発効率が大幅に向上します。

TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

おすすめ記事

おすすめコンテンツ