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