仕組みから学ぶ Web
応用読了目安 15#テスト#E2E#単体テスト#テストピラミッド

テスト戦略 — どこに何枚の網を張るか

テストは多いほど良いわけではない。壊れやすさ・実行時間・書く手間のバランスをどこで取るか。

この記事の進み方

What — テストの種類と性質

Figureテストピラミッド
E2E テスト+ 全部が繋がっているブラウザから実際に操作する数十本・数分〜数十分
結合テスト+ SQL・トランザクションDB や外部を含めて検証する数百本・数分
単体テスト+ ロジックの正しさ外部に依存しない部分を検証する数千本・数秒
静的解析+ 型・構文実行せずに分かる問題を潰す常時・数秒
このテストが保証する範囲
型・構文データ本体
1/4
静的解析型チェックと Lint。テストを1本も書かずに、大量の間違いを防げる。最も安いのでまず整える。

下ほど速く・安定し・原因が特定しやすい。上ほど遅く・不安定だが、本当に動くことを確認できる。数のバランスを下に厚く取る。

同じバグを、どの層で捕まえるか

Compare「割引率が 100% を超えるとマイナス金額になる」というバグ

計算ロジックだけを直接検証する。最も速く、原因も一目で分かる。

test('割引率は 100% を超えられない', () => {
expect(() => Discount.of(101)).toThrow();
});

実行時間: 1ms
落ちたとき: Discount のバリデーションが原因と即座に分かる
安定性: 100%
実行時間
1ms
原因の特定
即座
安定性
常に同じ結果
書く手間
3行
  • ドメイン層が独立していれば、この形で書ける。
  • 「不正な値を作れない」ことを検証するので、使う側すべてが守られる。

同じバグでも、どの層で捕まえるかでコストがまったく違う。下の層で捕まえられるものを上で捕まえるのは無駄。

何をテストするか

// 価値が低い:実装をなぞっているだけ
test('setName で名前が設定される', () => {
  user.setName('田中');
  expect(user.getName()).toBe('田中');
});

// 価値が高い:業務ルールを検証している
test('発送済みの注文はキャンセルできない', () => {
  const order = Order.shipped();
  expect(() => order.cancel(now)).toThrow(OrderCannotBeCancelled);
});

// 価値が高い:境界値
test('キャンセル期限は注文から7日ちょうどまで', () => {
  const order = Order.placedAt(day0);
  expect(order.canCancel(addDays(day0, 7))).toBe(true);
  expect(order.canCancel(addDays(day0, 7, 1))).toBe(false);
});

getter / setter のテストは、実装を写しただけで何も保証していません。 実装を変えればテストも変わるので、リファクタリングの邪魔にしかなりません。

テストすべきは、壊れたら困るルール境界値です。

確認 — ここまで読めたか

同じバグを捕まえられるなら、どの層のテストを選ぶべきですか?

まず選ぶ(解答例は a〜d の記号で説明します)

Why — なぜ E2E を増やすと遅くなるのか

E2E テストが持つ性質は、すべて「本物を使う」ことから来ています。

遅い。 ブラウザの起動、ページの読み込み、API の呼び出し、DB のアクセス。1本あたり数秒〜数十秒かかります。100本なら数十分。

不安定(フレーキー)。 実行のたびに結果が変わることがあります。原因は、

  • 非同期処理の完了を待ちきれない
  • アニメーションの途中でクリックする
  • テスト間でデータが干渉する
  • 外部 API の応答が遅い

原因が特定しにくい。 「注文が完了しない」という結果からは、フロント・API・DB・認証のどこが原因か分かりません。

壊れやすい。 画面のマークアップを変えただけで、ロジックは無傷なのにセレクタが見つからず落ちます。

フレーキーテストは即座に対処する

フレーキーなテストを1本許すと、次が起きます。

  1. 「このテストはたまに落ちる」という知識が共有される
  2. 落ちたら再実行するようになる
  3. 他のテストが落ちても、まず再実行するようになる
  4. 本物のバグを見逃す

1本のフレーキーテストが、テストスイート全体の信頼性を下げます。

対処は3つのいずれか。

  • 直す(最善)— 待機条件を明示する、テストデータを独立させる
  • 無効化する(次善)— 期限とチケットを付けて、一時的に外す
  • 削除する — 価値が低いなら消す

「たまに落ちるが放置」が最悪です。

テストしやすさは設計の結果

テストが書きにくいなら、設計に問題があることが多い。

// テストしにくい:外部に直接依存している
class OrderService {
  async create(input) {
    const order = await prisma.order.create({ ... });
    await stripe.charges.create({ ... });
    await sendgrid.send({ ... });
  }
}
// → テストに DB・Stripe・SendGrid のモックが必要
// テストしやすい:ルールが外部から独立している
class Order {
  canCancel(now) { ... }     // ← モック不要でテストできる
  cancel(now) { ... }
}

モックが5つ必要なら、5つに依存しています。 レイヤードアーキテクチャと依存性逆転を適用すると、テストの書きやすさが副産物として付いてきます。

確認 — ここまで読めたか

たまに落ちるテストを「再実行すれば通る」と扱い続けると、何が起きますか?

まず選ぶ(解答例は a〜d の記号で説明します)

演習 — まず自分で判断する

解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。

演習 — 設計判断を問う

このテストスイートを、どう作り直しますか?

与えられた条件
  • SaaS の管理画面。現在のテスト構成は以下
  • ・単体テスト 40 本(主に getter/setter とユーティリティ関数)
  • ・結合テスト 0 本
  • ・E2E テスト 180 本(ほぼ全画面・全機能を網羅)
  • ・CI の実行時間は 40 分、うち E2E が 36 分
  • ・1日 5〜10 本の E2E がランダムに落ちる
  • ・先月、料金計算のバグが本番に流出した(E2E は通っていた)
  • ・料金計算は、プラン・従量課金・割引・日割りが絡む複雑なロジック
この軸で考える
  • · 料金計算のバグが E2E をすり抜けたのはなぜか
  • · 180 本の E2E のうち、E2E でしか検証できないものはどれくらいあるか
  • · フレーキーテストを放置すると、何が起きているか
まず選ぶ(解答例は a〜d の記号で説明します)

演習 — 説明できるか

「E2E を180本まで増やしたのに、品質が上がった実感が無い」という状況を説明してください

与えられた条件
  • CI の実行時間は40分
  • 失敗しても原因の特定に時間がかかる
  • 減らす提案が後ろ向きに見えないようにしたい

読み終わりましたか?

読了にすると、これを前提とする記事がロードマップで開放されます。