Jest・Vitest・Playwrightを使った2026年版テスト戦略ガイド

Jest・Vitest・Playwrightの最新テスト戦略を解説。単体テスト・統合テスト・E2Eテストの効果的な組み合わせ方をマスターできます。

Sponsored

2026年版:Jest・Vitest・Playwrightを使った最新テスト戦略ガイド

Webアプリケーション開発において、テストの品質が製品品質を左右する時代です。2026年時点で、Jest、Vitest、Playwrightの三つのテストフレームワークは、単体テスト、統合テスト、E2Eテストの標準的なツールとして確立されています。本記事では、これらのツールを効果的に組み合わせた最新のテスト戦略を詳解します。

2026年のテスト環境における各ツールの役割分担

Vistestの台頭と業界トレンド

2026年現在、Vitest v2.5以上が主流となっており、Jestからの移行が加速しています。Vistestは以下の理由から、新規プロジェクトでの採用が急増しました:

Vistestの主な利点:

  • Vite統合による高速実行(Jest比で3~5倍の高速化)
  • ESMネイティブサポートによる互換性向上
  • TypeScript 5.0以上の完全サポート
  • マルチスレッド実行による並列処理の最適化

2026年版のVistestはワーカースレッド管理が大幅に改善され、大規模プロジェクト(1000以上のテストファイル)でも安定動作するようになりました。

Jestの位置付けの変化

Jest v30がリリースされた2026年初頭、Jestはレガシープロジェクトのメンテナンスや、特定の用途(スナップショットテスト、大規模な既存プロジェクト)に特化する方向性が明確化されました。新規プロジェクトではVistestが推奨される傾向にあります。

Playwrightの進化

Playwright v1.48では、AIアシスト機能を備えたテストジェネレーターが統合され、自動テスト作成の効率が大幅に向上しました。また、複数ブラウザの並列実行性能が2025年比で40%向上しています。

実践的なテスト戦略の構築

テストピラミッドの最新形

2026年のベストプラクティスは、従来のテストピラミッドから「テストトーナス」モデルへの転換です:

       E2Eテスト (Playwright)
      ↙          ↖
  統合テスト        API テスト
  (Vitest)       (Vitest)
    ↙              ↖
  単体テスト (Vitest)

推奨配分(2026年標準):

  • 単体テスト:45%(ビジネスロジック重点)
  • 統合テスト:30%(API連携、状態管理)
  • API/マイクロサービステスト:15%(バックエンド連携)
  • E2Eテスト:10%(クリティカルパス)

従来の80/15/5の配分から大きく変更されたのは、マイクロサービスアーキテクチャ普及とCI/CD高速化の需要からです。

Vistestを使った単体・統合テストの実装

セットアップと基本設定(2026年版)

// vitest.config.ts (Vitest v2.5以上)
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [vue(), react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html', 'lcov'],
      statements: 80,
      branches: 75,
      functions: 80,
      lines: 80,
    },
    // 2026年の標準設定:マルチスレッド有効
    threads: true,
    maxThreads: 4,
    minThreads: 1,
    // パフォーマンス監視(新機能)
    benchmark: {
      include: ['**/*.bench.ts'],
      outputFile: './test/benchmark-results.json',
    },
  },
});

実装例:Reactコンポーネントテスト

// src/components/Counter.test.tsx
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';

describe('Counterコンポーネント', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('初期値が0で表示される', () => {
    render(<Counter initialValue={0} />);
    expect(screen.getByText('Count: 0')).toBeInTheDocument();
  });

  it('ボタンクリックでカウント増加', async () => {
    const user = userEvent.setup();
    render(<Counter initialValue={0} />);
    
    const button = screen.getByRole('button', { name: /increment/i });
    await user.click(button);
    
    expect(screen.getByText('Count: 1')).toBeInTheDocument();
  });

  it('親からのコールバックが実行される', async () => {
    const handleChange = vi.fn();
    const user = userEvent.setup();
    
    render(<Counter initialValue={5} onChange={handleChange} />);
    await user.click(screen.getByRole('button', { name: /increment/i }));
    
    await waitFor(() => {
      expect(handleChange).toHaveBeenCalledWith(6);
    });
  });

  // パフォーマンステスト(2026年新機能)
  it.bench('1000回のレンダリング', () => {
    for (let i = 0; i < 1000; i++) {
      render(<Counter initialValue={i} />);
    }
  });
});

API統合テストの例

// src/services/api.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { fetchUserData } from './api';

const server = setupServer(
  http.get('/api/users/:id', ({ params }) => {
    const { id } = params;
    return HttpResponse.json({
      id,
      name: 'Test User',
      email: 'test@example.com',
      createdAt: '2026-01-15T10:30:00Z',
    });
  })
);

describe('APIサービス', () => {
  beforeEach(() => server.listen());
  afterEach(() => server.resetHandlers());

  it('ユーザーデータを正常に取得', async () => {
    const user = await fetchUserData('123');
    
    expect(user).toEqual({
      id: '123',
      name: 'Test User',
      email: 'test@example.com',
    });
  });

  it('ネットワークエラーを適切に処理', async () => {
    server.use(
      http.get('/api/users/:id', () => {
        return HttpResponse.error();
      })
    );

    await expect(fetchUserData('123')).rejects.toThrow(
      'Failed to fetch user data'
    );
  });

  // 2026年の標準:リトライロジックテスト
  it('ネットワーク障害時の自動リトライが機能', async () => {
    let attempts = 0;
    server.use(
      http.get('/api/users/:id', () => {
        attempts++;
        if (attempts < 3) {
          return HttpResponse.error();
        }
        return HttpResponse.json({ id: '123', name: 'Test User' });
      })
    );

    const user = await fetchUserData('123', { maxRetries: 3 });
    expect(user.name).toBe('Test User');
    expect(attempts).toBe(3);
  });
});

PlaywrightによるE2Eテスト戦略

Playwright設定(2026年最新版)

// playwright.config.ts (Playwright v1.48)
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['json', { outputFile: 'test-results/e2e.json' }],
    ['junit', { outputFile: 'test-results/e2e.xml' }],
  ],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    // 2026年新機能:AI生成ロケータ
    enableAITestGeneration: true,
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    // モバイルテスト
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

クリティカルパスのE2Eテスト実装

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

test.describe('チェックアウトフロー', () => {
  test.beforeEach(async ({ page }) => {
    // テストデータベースのリセット
    await page.request.post('/api/test/reset');
    await page.goto('/');
  });

  test('商品選択からペイメントまでの完全フロー', async ({ page }) => {
    // 1. 商品検索
    await page.getByPlaceholder('商品を検索').fill('テスト商品');
    await page.getByRole('button', { name: /検索/i }).click();
    
    await expect(page.getByText('テスト商品')).toBeVisible();

    // 2. 商品を選択
    await page.getByText('テスト商品').click();
    await expect(page).toHaveURL(/\/product\/\d+/);

    // 3. カートに追加
    await page.getByRole('button', { name: /カートに追加/i }).click();
    await expect(page.getByText('カートに追加されました')).toBeVisible();

    // 4. チェックアウトへ
    await page.getByRole('link', { name: /カートへ/i }).click();
    await expect(page).toHaveURL(/\/cart/);

    // 5. 配送情報入力
    await page.getByLabel('住所').fill('東京都渋谷区 テスト住所');
    await page.getByLabel('電話番号').fill('09012345678');
    await page.getByRole('button', { name: /次へ/i }).click();

    // 6. ペイメント情報
    await page.getByLabel('クレジットカード番号').fill('4242424242424242');
    await page.getByLabel('有効期限').fill('12/26');
    await page.getByLabel('CVC').fill('123');

    // 7. 注文確定
    await page.getByRole('button', { name: /注文を確定/i }).click();

    // 8. 完了画面の確認
    await expect(page).toHaveURL(/\/order-complete/);
    await expect(page.getByText('注文ありがとうございます')).toBeVisible();

    // 確認メールが送信されたことを検証
    const emailResponse = await page.request.get(
      '/api/test/emails?to=test@example.com'
    );
    expect(emailResponse.ok()).toBeTruthy();
  });

  // 2026年の推奨:

Sponsored

関連記事