MENU

フロントエンドテスティング完全ガイド – Jest・Vitest・Playwright

目次

フロントエンドテスティング完全ガイド – Jest・Vitest・Playwright

テスティングの戦略

フロントエンドテスティングは品質保証の基盤です。ユニットテスト、統合テスト、E2E テストの 3 層構造により、本番環境でのバグを最小限に抑えられます。Jest、Vitest、Playwright という 3 つの主要なテストフレームワークの特徴と使い分けをまとめました。

対象となる方

  • フロントエンドエンジニア、テスト品質を重視する開発チーム
  • テスト導入を検討している方
  • 既存テストの改善を目指す方

※ このドキュメントは React 環境を想定しています。

テストピラミッドの理解

テストの 3 階層

        E2E テスト(少数)
      /    \
     /      \
    /統合テスト\
   /          \
  /ユニットテスト\
 /              \

ユニットテスト:関数やコンポーネント単位の最小単位のテスト。最も数が多く、実行も高速。

統合テスト:複数のコンポーネントやモジュール組み合わせた機能テスト。

E2E テスト:エンドユーザーの操作をシミュレートしたテスト。最も重要だが、実行時間が長い。

Jest – フロントエンド標準テストフレームワーク

Jest の特徴

Jest は Facebook(Meta)が開発したテストフレームワークで、Create React App にデフォルトで含まれています。ゼロコンフィグで動作し、スナップショットテストなど独特の機能があります。

// Jest セットアップ
npm install --save-dev jest @testing-library/react @testing-library/jest-dom

// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['/src/setupTests.js'],
};

Jest でのユニットテスト

// sum.js - テスト対象
export function sum(a, b) {
  return a + b;
}

// sum.test.js - テストコード
import { sum } from './sum';

describe('sum関数', () => {
  test('正の数の合計を計算', () => {
    expect(sum(1, 2)).toBe(3);
  });

  test('負の数に対応', () => {
    expect(sum(-1, -2)).toBe(-3);
  });

  test('0を返す', () => {
    expect(sum(0, 0)).toBe(0);
  });
});

Jest でのコンポーネントテスト

// Button.jsx
export function Button({ label, onClick, disabled }) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}

// Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

describe('Button コンポーネント', () => {
  test('ボタンがレンダリングされる', () => {
    render(<Button label="Click me" onClick={() => {}} />);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  test('クリックで onClick が呼ばれる', () => {
    const handleClick = jest.fn();
    render(<Button label="Click" onClick={handleClick} />);
    fireEvent.click(screen.getByText('Click'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  test('disabled 属性が適用される', () => {
    render(<Button label="Disabled" onClick={() => {}} disabled />);
    expect(screen.getByText('Disabled')).toBeDisabled();
  });
});

Jest のスナップショットテスト

// Card.test.jsx
import { render } from '@testing-library/react';
import { Card } from './Card';

describe('Card コンポーネント', () => {
  test('スナップショットテスト', () => {
    const { container } = render(
      <Card title="Test" description="Description" />
    );
    expect(container).toMatchSnapshot();
  });
});

注意:スナップショットテストは UI の変更を検出しますが、意図しない変更をキャッチするためのツールです。頻繁に更新される必要があります。

Vitest – 高速なテストランナー

Vitest の特徴

Vitest は Jest 互換で、Vite による高速テスト実行が特徴です。Jest より 2~3 倍高速で実行でき、HMR(ホットモジュールリプレースメント)でテストを自動実行できます。

// vitest.config.js
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/setup.ts',
  },
})

Vitest でのテスト実行

# Vitest セットアップ
npm install --save-dev vitest jsdom @testing-library/react

# ウォッチモードで実行(テスト自動再実行)
npm run test

# テストカバレッジを表示
npm run test -- --coverage

# UI でテスト結果を表示
npm run test -- --ui

Vitest のメリット

  • 高速実行:Jest より 2~3 倍高速
  • Jest 互換:既存 Jest テストを流用可能
  • HMR 対応:ファイル保存で自動実行
  • UI 表示:ブラウザでテスト結果を可視化
  • Vite との統合:設定が共有できる

Playwright – E2E テストの最強ツール

Playwright の特徴

Playwright は Chromium、Firefox、WebKit という 3 つのブラウザエンジンをコントロールし、クロスブラウザ E2E テストを実現します。実際のブラウザでエンドユーザーの操作をシミュレートできます。

# Playwright セットアップ
npm install -D @playwright/test

# プロジェクト初期化
npx playwright install

Playwright でのテスト実装

// login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('ログイン機能', () => {
  test('ユーザーがログインできる', async ({ page }) => {
    // ログインページに移動
    await page.goto('http://localhost:3000/login');

    // ユーザー名を入力
    await page.fill('input[name="username"]', 'testuser');

    // パスワードを入力
    await page.fill('input[name="password"]', 'password123');

    // ログインボタンをクリック
    await page.click('button:has-text("ログイン")');

    // ダッシュボードページに遷移したか確認
    await expect(page).toHaveURL('http://localhost:3000/dashboard');

    // ウェルカムメッセージが表示されているか
    await expect(page.locator('h1')).toContainText('ようこそ');
  });

  test('無効な認証情報でエラーが表示される', async ({ page }) => {
    await page.goto('http://localhost:3000/login');
    await page.fill('input[name="username"]', 'invalid');
    await page.fill('input[name="password"]', 'wrong');
    await page.click('button:has-text("ログイン")');

    // エラーメッセージが表示される
    await expect(page.locator('.error-message')).toBeVisible();
  });
});

Playwright でのスクリーンショット・動画録画

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,

  // スクリーンショット設定
  use: {
    screenshot: 'only-on-failure',  // 失敗時のみスクショ
    video: 'retain-on-failure',      // 失敗時のみ動画保存
  },

  // 複数ブラウザでテスト
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

React コンポーネントテスト(React Testing Library)

React Testing Library の哲学

React Testing Library は「ユーザーが使うように」テストすることを推奨しています。内部実装ではなく、ユーザーが見える UI をテストします。

// ❌ 良くないテスト(内部実装に依存)
test('state を確認', () => {
  const { getByTestId } = render(<Counter />);
  const stateValue = getByTestId('counter-state').textContent;
  expect(stateValue).toBe('0');
});

// ✅ 良いテスト(ユーザー視点)
test('ボタンをクリックでカウント増加', () => {
  render(<Counter />);
  const button = screen.getByRole('button', { name: /increment/i });
  const display = screen.getByText('0');

  fireEvent.click(button);

  expect(screen.getByText('1')).toBeInTheDocument();
});

テスト対象の優先順位

// テスト対象の優先順位(上から順に推奨)
// 1. getByRole - アクセシビリティ属性を使用
screen.getByRole('button', { name: /submit/i });

// 2. getByLabelText - ラベル経由
screen.getByLabelText('Username');

// 3. getByPlaceholderText - プレースホルダ経由
screen.getByPlaceholderText('Enter name');

// 4. getByText - テキスト内容経由
screen.getByText('Submit');

// 5. getByTestId - テスト用 ID(最終手段)
screen.getByTestId('submit-button');

テストカバレッジと品質指標

カバレッジの種類

種類 説明 目安
ステートメント 実行されたコード行数 80%
ブランチ 条件分岐の網羅率 75%
関数 定義された関数の実行率 80%
ライン 物理的な行数の網羅率 80%
# カバレッジレポート生成
npm run test -- --coverage

# 出力例
# ─────────────────────────────────────
# File      | Stmts | Branch | Funcs
# ─────────────────────────────────────
# sum.js    | 100%  | 100%   | 100%
# button.js | 85%   | 75%    | 90%
# ─────────────────────────────────────

CI/CD パイプラインでのテスト自動化

GitHub Actions でのテスト実行

# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Node.js セットアップ
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: 依存をインストール
        run: npm ci

      - name: ユニットテスト実行
        run: npm run test

      - name: ビルド確認
        run: npm run build

      - name: E2E テスト実行
        run: npm run test:e2e

テストフレームワーク比較表

項目 Jest Vitest Playwright
テスト種別 ユニット・統合 ユニット・統合 E2E
実行速度 中程度 高速 遅い(ブラウザ起動)
セットアップ 簡単 簡単 中程度
デバッグ性 優秀 優秀 スクショ・動画
クロスブラウザ × × ○(3 ブラウザ)
HMR 対応 × ○ ×

ベストプラクティス

テスト戦略

  • テストは品質を保証するためのツール、すべてをカバーする必要はない
  • ユニットテストで 70~80%、E2E で重要フロー のみカバー
  • バグが発生した箇所からテストを追加する(テスト駆動開発)
  • スナップショットテストは過度に使用しない

テスト作成のコツ

  • AAA パターン:Arrange(準備)→ Act(実行)→ Assert(検証)
  • 1 テスト 1 概念:複数の検証を避ける
  • わかりやすい名前:テスト名は何をテストしているか明確に
  • データ分離:テスト間でデータを共有しない

まとめ

フロントエンドテスティングは品質保証の基盤です。テストピラミッド構造で各レイヤーのテストを組み合わせることで、堅牢なアプリケーションが実現できます:

  • Jest:フロントエンド標準。ユニット・統合テストに使用
  • Vitest:Jest 互換で高速。Vite プロジェクトに推奨
  • Playwright:E2E テストの最強ツール。実ブラウザでテスト

単位テスト・統合テスト・E2E テストをバランスよく実施し、自動化によりコード品質を継続的に保証しましょう。テストがあれば、安心してリファクタリングや機能追加ができます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次