コンテンツにスキップ

E2Eテストを単体テストに置き換える

ユースケース: E2Eスイートは40分かかり、なお成長し続けている。しかしその多くは、ブラウザを起動し、ログインし、3画面遷移して……割引が正しく計算されるかを確認しているだけ。ブラウザの税金を払わずに、同じ確信だけを手に入れたい。

E2Eテストを削除するのはカバレッジを削除するように感じられます。必要なのは、テストのどの部分が本当にブラウザを必要としているかを見抜く目です。ほとんどのE2Eテストは、1つのジャーニー(E2Eが必要)と多数の判定ロジック(不要)を束ねています。この判断を代行するツールはありませんが、繰り返し使える手順はあります。

アサーションが統合ではなく計算結果についてのテストは、降格候補です:

  • アサート対象が入力から導出されるテキスト: 価格、バリデーションメッセージ、日付フォーマット、権限(「管理者には削除ボタンが見える」)。
  • 同じテストがデータ違いでN回存在する — 手順は同じでテーブルの行だけ違う。
  • 書かれた目的の理由で失敗したことがなく、落ちるのはセレクターとタイミングのせいばかり。

テストランナーのデータも助けになります: (実行時間 × 失敗率 ÷ 発見したユニークなバグ数)でソートすれば、遅くてフレーキーで一度もリグレッションを捕まえたことのないテストが行列の先頭に並びます。

降格を安全にするリファクタリングは「ロジックを純粋関数に移し、その関数をテストする」です。

// Before: UIを通してしかテストできない
async function onCheckout() {
const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);
const discount = user.isMember && subtotal > 5000 ? subtotal * 0.05 : 0;
render(`Total: ${format(subtotal - discount + shippingFor(subtotal))}`);
}
// After: 判定がユニットになった
export function checkoutTotal(items: Item[], user: User): Money { /* … */ }

10本のE2Eバリエーション(会員/非会員 × 閾値 × 配送料区分)は、ミリ秒で走るテーブル駆動の単体テストになります:

test.each`
items | member | total
${[shirt(5001)]} | ${true} | ${4750.95}
${[shirt(5001)]} | ${false} | ${5001.0}
${[shirt(4999)]} | ${true} | ${4999.0}
`('checkout total', ({ items, member, total }) => {
expect(checkoutTotal(items, user({ member }))).toEqual(money(total));
});

チェックアウトを通るE2Eジャーニーはちょうど1本残します — その仕事は「配線が保たれていること」に縮小されます: 関数の結果が実際に画面へ届き、注文が実際にデータベースへ着地すること。

3. UIの判定にはコンポーネントテスト

Section titled “3. UIの判定にはコンポーネントテスト”

「管理者には削除ボタンが見える」の確認にデプロイ済みのシステムは要りません — 両方のロールでコンポーネントをレンダリングすれば済みます(Vitestブラウザモード / Testing Library)。ブラウザのセマンティクスは保ちつつ、バックエンドなし、ログインなし、どちら由来のフレークもなし。

降格はペアで進めます: まず単体/コンポーネントテストを先に入れ、旧E2Eテストは @demoted タグを付けて数週間走らせ続け、その後削除します。この期間中に下層のテストがE2Eの捕まえた何かを見逃したら、統合リスクが実際にどこにあるかを学べたことになります — テストピラミッドはその知識の上に築かれるものです。

  • ジャーニーは降格しないこと:「サインアップ → メール確認 → 初回購入」は定義からして統合です — トランザクションメールをテストするを参照。
  • ロジックが抽出できない(ハンドラーに絡みついている)なら、それはテスト負債が指し示すアプリ側のリファクタリング課題です — テストスイートは伝令にすぎません。
  • 降格の過程ではユースケースカバレッジを追跡します: カバレッジはピラミッドの下へ移動すべきで、消えてはいけません。