テスト戦略 — どこに何枚の網を張るか
テストは多いほど良いわけではない。壊れやすさ・実行時間・書く手間のバランスをどこで取るか。
この記事の進み方
What — テストの種類と性質
下ほど速く・安定し・原因が特定しやすい。上ほど遅く・不安定だが、本当に動くことを確認できる。数のバランスを下に厚く取る。
同じバグを、どの層で捕まえるか
計算ロジックだけを直接検証する。最も速く、原因も一目で分かる。
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 のテストは、実装を写しただけで何も保証していません。 実装を変えればテストも変わるので、リファクタリングの邪魔にしかなりません。
テストすべきは、壊れたら困るルールと境界値です。
同じバグを捕まえられるなら、どの層のテストを選ぶべきですか?
Why — なぜ E2E を増やすと遅くなるのか
E2E テストが持つ性質は、すべて「本物を使う」ことから来ています。
遅い。 ブラウザの起動、ページの読み込み、API の呼び出し、DB のアクセス。1本あたり数秒〜数十秒かかります。100本なら数十分。
不安定(フレーキー)。 実行のたびに結果が変わることがあります。原因は、
- 非同期処理の完了を待ちきれない
- アニメーションの途中でクリックする
- テスト間でデータが干渉する
- 外部 API の応答が遅い
原因が特定しにくい。 「注文が完了しない」という結果からは、フロント・API・DB・認証のどこが原因か分かりません。
壊れやすい。 画面のマークアップを変えただけで、ロジックは無傷なのにセレクタが見つからず落ちます。
フレーキーテストは即座に対処する
フレーキーなテストを1本許すと、次が起きます。
- 「このテストはたまに落ちる」という知識が共有される
- 落ちたら再実行するようになる
- 他のテストが落ちても、まず再実行するようになる
- 本物のバグを見逃す
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つに依存しています。 レイヤードアーキテクチャと依存性逆転を適用すると、テストの書きやすさが副産物として付いてきます。
たまに落ちるテストを「再実行すれば通る」と扱い続けると、何が起きますか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
このテストスイートを、どう作り直しますか?
- SaaS の管理画面。現在のテスト構成は以下
- ・単体テスト 40 本(主に getter/setter とユーティリティ関数)
- ・結合テスト 0 本
- ・E2E テスト 180 本(ほぼ全画面・全機能を網羅)
- ・CI の実行時間は 40 分、うち E2E が 36 分
- ・1日 5〜10 本の E2E がランダムに落ちる
- ・先月、料金計算のバグが本番に流出した(E2E は通っていた)
- ・料金計算は、プラン・従量課金・割引・日割りが絡む複雑なロジック
- · 料金計算のバグが E2E をすり抜けたのはなぜか
- · 180 本の E2E のうち、E2E でしか検証できないものはどれくらいあるか
- · フレーキーテストを放置すると、何が起きているか
「E2E を180本まで増やしたのに、品質が上がった実感が無い」という状況を説明してください
- CI の実行時間は40分
- 失敗しても原因の特定に時間がかかる
- 減らす提案が後ろ向きに見えないようにしたい
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。