はじめに

Reactフロントエンド開発において、E2Eテストツール「MagicPod」とコンポーネントテストをどう使い分けるべきか。この問いに対する明確な答えを持たないまま、テストを書いているチームは少なくありません。本記事では、Kent C. Doddsが提唱した「Testing Trophy」の考え方を軸に、両者の境界を合理的に設計するための指針を提示します。

注意

本記事で述べるテスト戦略は、React を用いた SPA(Single Page Application)を前提としています。

Java + JSP / Thymeleaf / Struts などのサーバーサイドレンダリング(SSR)型アーキテクチャでは、画面遷移や状態管理、テスト対象の粒度が本質的に異なるため、同一の Testing Trophy 構成や MagicPod と Integration Tests の境界設計は成立しません。

特に、JSP を中心とした構成では「画面=リクエスト境界」となり、React SPA のように単一画面内で状態・副作用・UI が完結する前提が存在しないため、本稿の戦略をそのまま適用することは適切ではありません。

Testing Trophyとは何か

従来のテストピラミッドは、ユニットテストを最も多く書き、上層に行くほどテスト数を減らすという考え方でした。このモデルは、バックエンドAPIに対しては有効ですが、フロントエンドに適用すると実装の詳細に依存したテストを大量に生み出し、リファクタリングのたびにテストが壊れるという問題を引き起こします。

Testing Trophyは、この問題に対するアンチテーゼとして生まれました。トロフィーの形状が示すように、最も厚みを持つべきは中央のIntegration Tests層です。

参考:Kent C. Dodds The Testing Trophy and Testing Classifications

この構造が意味するのは、「ユーザーがアプリケーションを使う方法に近いテストほど、信頼性が高い」という原則です。

MagicPodでのテストはTesting Trophyのどこに位置するか

MagicPodはE2Eテストツールであり、Testing Trophyの最上部に位置します。トロフィーの形状が示す通り、この層は意図的に薄く保つべきです。

理由は明確で、E2Eテストは実行時間が長く、フレーキー(Flaky)になりやすく、メンテナンスコストが高いためです。

しかし「薄く保つ」ことと「不要である」ことは異なります。E2E層でしか検証できない領域が確実に存在し、そこにMagicPodの真価があります。

参考:MagicPod E2Eテストとは?自動化のメリット・デメリットと手法まで徹底解説

各層の責務と境界の明確化

Static Analysis層:ESLintとFormatter

この層はテストを書く前の防波堤です。型エラーやリンターエラーをCIで検出することで、そもそもテストを書くまでもないバグを排除します。

この層で守るべきこととして、propsの型不整合、未使用変数や到達不能コード、アクセシビリティ属性の欠落などが挙げられます。

MagicPodとの境界という観点では、この層とMagicPodは完全に独立しています。

Unit Tests層:純粋ロジックに限定する

Testing Trophyにおいて、Unit Testsの対象は「純粋なロジック」に厳密に限定されます。コンポーネントのレンダリング結果をテストするのではなく、コンポーネントから切り出された計算ロジックや変換関数をテストします。

具体的なテスト対象としては、日付フォーマット関数、価格計算ロジック、バリデーションルール、状態遷移のリデューサー、APIレスポンスの正規化処理などがあります。

// Unit Testの適切な対象例
export function calculateTaxIncludedPrice(price: number, taxRate: number): number {
  return Math.floor(price * (1 + taxRate));
}

// このような純粋関数はUnit Testで検証します
describe('calculateTaxIncludedPrice', () => {
  it('税込価格を計算し、小数点以下を切り捨てる', () => {
    expect(calculateTaxIncludedPrice(1000, 0.1)).toBe(1100);
    expect(calculateTaxIncludedPrice(999, 0.1)).toBe(1098);
  });
});

MagicPodとの境界について述べると、Unit Testsで検証するロジックは、UIから完全に分離されているべきです。

したがって、MagicPodで同じロジックをテストする必要は一切ありません。ロジックの正しさはUnit Testsが保証し、そのロジックがUIに正しく統合されているかはIntegration Testsが保証します。

Integration Tests層:Testing Trophyの中核

ここがTesting Trophyの核心であり、フロントエンドテスト戦略において最も投資すべき層です。React Testing Library(RTL)とMSW(Mock Service Worker)を組み合わせ、「ユーザーの視点で」コンポーネントの振る舞いを検証します。

Integration Testsでは、ユーザー操作に対するUI状態の変化、フォーム入力からサブミットまでの一連の流れ、APIレスポンスに応じた表示の切り替え、エラー状態のハンドリング、ローディング状態の表示などを検証します。

// Integration Testの例
describe('商品検索フォーム', () => {
  it('検索実行後、結果一覧が表示される', async () => {
    // MSWでAPIをモック
    server.use(
      rest.get('/api/products', (req, res, ctx) => {
        return res(ctx.json([
          { id: 1, name: 'テスト商品A' },
          { id: 2, name: 'テスト商品B' },
        ]));
      })
    );

    render(<ProductSearchPage />);

    // ユーザーの操作をシミュレート
    await userEvent.type(screen.getByRole('searchbox'), 'テスト');
    await userEvent.click(screen.getByRole('button', { name: '検索' }));

    // 結果の検証
    expect(await screen.findByText('テスト商品A')).toBeInTheDocument();
    expect(screen.getByText('テスト商品B')).toBeInTheDocument();
  });
});

ここで重要なのは、MagicPodとの境界です。Integration Testsは「単一画面内」の振る舞いを検証します。MSWによってAPIをモックしているため、バックエンドの実装に依存しません。これにより、高速かつ安定したテスト実行が可能になります。

一方、MagicPodが担うべきは「画面をまたぐフロー」や「実際のバックエンドとの連携」です。この境界を明確に保つことで、テストの重複を避けつつ、それぞれの層が持つ強みを最大化できます。

E2E Tests層:MagicPodの戦略的活用

Testing Trophyの最上部に位置するE2E層は、意図的に薄く保ちます。MagicPodで検証すべきは、Integration Testsでは検証できない、あるいは検証する意味がない領域に限定されます。

MagicPodでテストすべき対象として、複数画面にまたがるユーザージャーニー(会員登録→ログイン→商品購入→決済完了)、OAuth認証フロー(実際のリダイレクトを含む)、PDF・帳票出力(実ファイル生成の確認)、メール送信トリガーの確認があります。

これらは、モックで再現しても本質的な価値を検証できない領域です。実際のシステム間連携が正しく動作することを確認するには、E2Eテストが不可欠です。

境界設計のアンチパターン

実際のプロジェクトでよく見られる、避けるべきパターンを挙げます。

Integration TestsでAPIを実際に呼び出す

MSWを使わず、実際のAPIエンドポイントを呼び出すテストを書いているケースがあります。これはE2E層の責務を侵食しており、テストの安定性を損ないます。Integration層ではMSWでモックし、実APIとの連携はE2E層に任せるべきです。

MagicPodでコンポーネント単体をテストする

ボタンをクリックしたらモーダルが開く、という振る舞いをMagicPodでテストしているケースがあります。これはIntegration Testsで十分にカバーできる領域であり、E2Eで行う意味がありません。実行時間が長くなり、DOMセレクタの変更で頻繁に壊れ、メンテナンスコストだけが増大します。

まとめ:境界を整理し「信頼性」を積む

テストレイヤー 目的・位置づけ 使用技術・手段 投資比率(目安) コスト構造・方針
Static Analysis 実行前に欠陥を潰す最下層の品質担保。型・規約・設計逸脱の早期検出 TypeScript strict モード、ESLint(厳格ルール) ―(コード量非依存) 運用・保守コストは極小
Unit Tests 純粋ロジックの正当性検証。分岐・計算・変換処理の担保 Vitest / Jest 等 10〜15% 対象を限定することで過剰投資を防止。UI・副作用は扱わない
Integration Tests ユーザー視点での振る舞い検証の中核。画面+状態+API連携 React Testing Library(RTL)、MSW 60〜70% 最重要投資。仕様変更に強く、ROIが最も高い
E2E Tests 実環境での致命的破綻防止。業務クリティカルパスの担保 MagicPod 15〜20% 範囲を厳選し、壊れやすさと保守コストを制御

Integration Testsで十分な信頼性が得られる領域にE2Eを持ち込む必要はありません。逆に、実際のシステム連携でしか検証できない領域をIntegration Testsで無理にカバーしようとすべきではありません。

Testing Trophyが示すのは、「テストの信頼性」と「テストのコスト」のバランスです。ユーザーの操作に近いテストほど信頼性は高まりますが、同時にコストも増大します。MagicPodによるE2Eテストとフロントエンドテストの境界は、このトレードオフを意識して設計すべきです。

Appendix: TISにおけるテスト環境の整備状況

TISでは、本記事で述べたTesting Trophyの考え方に基づき、フロントエンドテスト環境の整備を進めています。

現在、以下のツールとバージョンを採用し、テストインフラの構築を行っています。

  • Storybook 10: コンポーネントの可視化とビジュアルリグレッションテストの基盤として活用
  • Vitest 4: Unit TestsおよびIntegration Testsの実行環境として採用

Storybookは、コンポーネントの開発環境としてだけでなく、デザインシステムのドキュメント化やアクセシビリティチェックの自動化にも活用しています。Vitestは、高速な実行速度とViteとのシームレスな統合により、開発者体験の向上に貢献します。

これらのツールを組み合わせることで、本記事で提示したTesting Trophy戦略を実践し、持続可能なテスト体制の構築を目指しています。

参考リンク