「テストを書いているのに、リファクタリングのたびに壊れる」「何をどこまでテストすれば良いかわからない」——そんな悩みを抱えるフロントエンドエンジニアは少なくありません。テスト戦略が曖昧なまま開発が進むと、テストコードはやがてメンテナンスの足かせになってしまいます。
本記事では、現在のフロントエンド開発でスタンダードになりつつある Vitest と Playwright を軸に、「壊れにくく、意味のあるテスト」を実現するモダンなテスト戦略を体系的に解説します。
技術的背景:なぜ今 Vitest と Playwright なのか
かつてフロントエンドのユニットテストは Jest が長年にわたって主流でしたが、Vite ベースのプロジェクトが普及するにつれ、その相性の悪さが課題になってきました。Jest は内部的に Babel によるトランスパイルに依存しており、Vite の ESM ネイティブな高速ビルドと噛み合わないケースが多く発生します。
そこで登場したのが Vitest です。Vite の設定とトランスパイラをそのまま共有できるため、ビルドパイプラインを統一でき、テスト実行のウォッチモードも非常に軽快です。Babel や ts-jest のための追加設定が不要な点も、環境構築のコストを大幅に下げてくれます。
E2E テストの領域では、Playwright がデファクトスタンダードとして定着しつつあります。Chromium・Firefox・WebKit の複数ブラウザへの対応、テストの並列実行、そして強力なデバッグ UI(Trace Viewer)を備えており、以前主流だった Cypress と比較しても、クロスブラウザ対応や CI 環境での安定性で一歩リードしています。
さらに、MSW(Mock Service Worker) を組み合わせることで、API サーバーを起動せずにネットワーク層をモック化できます。実環境に近い条件でテストを実行しながら、外部サービスへの依存をなくした安定したテスト環境を構築できるのが大きな強みです。
テスト戦略の指針:テスティングトロフィー
モダンなテスト設計において最も重要な考え方が、Kent C. Dodds 氏が提唱する テスティングトロフィー(Testing Trophy) です。従来の「テストピラミッド」がユニットテストを最重視するのに対し、テスティングトロフィーは 結合テスト(Integration)を中心に据える 点が特徴です。
-
Static(静的解析)
ESLint による構文チェックや TypeScript による型検査。コードを実行する前に問題を検出できるため、コストパフォーマンスが最も高い層です。 -
Unit(単体テスト)
純粋な関数やユーティリティのロジック検証に適しています。外部依存のない処理に絞って適用することで、高速かつ安定したテストを維持できます。 -
Integration(結合テスト)
複数のコンポーネントが連携して正しく動作するかを検証します。Testing Library を使い、DOM の内部構造ではなく「アクセシビリティ属性(Role)や表示テキスト」に基づいてテストを書くことで、実装の詳細に引きずられないリファクタリング耐性の高いテストが実現します。 -
E2E(エンドツーエンドテスト)
Playwright を使用し、実際のブラウザ上でユーザーが辿る一連のフローを検証します。ログイン→商品購入→完了画面といったクリティカルなシナリオに絞って適用するのが効果的です。
トロフィーの形が示すとおり、結合テストに最も多くのテストケースを集中させる のが基本的な考え方です。「ユーザーが見る画面の振る舞い」を検証することで、コンポーネントの内部実装を変えてもテストが壊れにくくなり、テストコードが長期的な資産として機能します。
エンジニアVitest は Vite のトランスパイラを直接利用するため、テスト用に別途 Babel や ts-jest の設定をする必要がありません。ウォッチモードの起動も一瞬で、開発中のフィードバックループが格段に速くなりますよ。
デザイナーPlaywright のビジュアルリグレッションテスト(スクリーンショット比較)を使えば、意図しない CSS の崩れに自動で気づけます。エンジニアだけでなく、デザインの品質を守る手段としても活用できますね。
実装例
1. Vitest の環境構築
ブラウザ API をエミュレートする場合は、vitest.config.ts で environment を設定します。jsdom が一般的ですが、より高速な happy-dom も選択肢に入ります。
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom', // または 'happy-dom'
setupFiles: './src/test/setup.ts', // jest-dom の matchers 拡張などを記述
},
});
2. Testing Library + user-event によるテスト記述
user-event は setup() でインスタンス化し、await で非同期に操作をシミュレートするのが現在の推奨パターンです。getByRole や getByText を使うことで、実装の詳細ではなくユーザー視点の要素取得が実現します。
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect } from 'vitest';
import { Counter } from './Counter';
describe('Counter Component', () => {
it('ボタンをクリックするとカウントが増加すること', async () => {
const user = userEvent.setup();
render( );
// 初期状態の確認
expect(screen.getByText(/count is 0/i)).toBeInTheDocument();
// ユーザー操作のシミュレーション(Role ベースで取得)
const button = screen.getByRole('button', { name: /increment/i });
await user.click(button);
// 操作後の状態を確認
expect(screen.getByText(/count is 1/i)).toBeInTheDocument();
});
});
まとめ
- Vitest を採用することで、Vite 開発環境と設定を共通化しつつ、高速で軽快なテスト実行環境を構築できる。
- テスティングトロフィー に基づき、結合テストを中心に据えることで、リファクタリングに強くメンテナンスコストの低いテスト設計が実現する。
- Testing Library × MSW の組み合わせにより、「実装の詳細」ではなく「ユーザーの振る舞い」を検証する質の高い結合テストを書ける。
- Playwright はクリティカルなユーザーフローに絞って適用し、ブラウザレベルの最終的な品質担保として機能させる。