テストピラミッド¶
テストピラミッド(Test Pyramid) は、Mike Cohnが提唱したテスト自動化の理想的な構成を表すモデル。
△
/E2E\ ← 少ない(遅い・高コスト)
/─────\
/統合テスト\ ← 中程度
/───────────\
/ 単体テスト \ ← 多い(速い・低コスト)
/───────────────\| 層 | 内容 | 特性 |
|---|---|---|
| 単体テスト(Unit) | 関数・クラス単位の動作確認 | 高速・安定・低コスト |
| 統合テスト(Integration) | 複数モジュールや外部システムとの連携確認 | 中程度の速度・コスト |
| E2Eテスト | ユーザー操作を再現した全体通しのテスト | 低速・高コスト・壊れやすい |
テストピラミッドのアンチパターン¶
逆に、次のような形はよくないとされる
アイスクリームコーン(逆ピラミッド) :E2Eが多く単体テストが少ない状態。実行が遅く、CI が詰まりやすい
砂時計 :単体テストとE2Eは多いが統合テストが少ない状態。モジュール間の結合バグを見逃しやすい
Googleでは単体テスト / 統合テストなどの区分ではなく、実行に必要なリソース量などでsmall, medium, largeに分ける。
参考:
Winters, T., Wright, H. (2021). Googleのソフトウェアエンジニアリング: 持続可能なプログラミングを支える技術、文化、プロセス. 日本: オライリー・ジャパン.
テストトロフィー¶
テストトロフィー(Testing Trophy) は、Kent C. Doddsが提唱したモデルで、フロントエンド開発における現代的なテスト構成の考え方。
△
/E2E\ ← 少ない
/─────\
/ 統合テスト \ ← 最も多い(中心)
/─────────────\
/ 単体テスト \ ← 適度に
/─────────────────\
/ 静的解析(型など) \ ← 基盤
/─────────────────────\テストピラミッドと異なり、統合テストを最も重視する。ユーザーの実際の使い方に近い粒度でテストすることが信頼性につながるという考え方。
“Write tests. Not too many. Mostly integration.” — Guillermo Rauch
テストピラミッド vs テストトロフィー¶
| 観点 | テストピラミッド | テストトロフィー |
|---|---|---|
| 最重視する層 | 単体テスト | 統合テスト |
| 提唱背景 | バックエンド・ロジック中心 | フロントエンド・UIコンポーネント |
| 代表ツール | JUnit, pytest | React Testing Library |
どちらが正解というわけではなく、プロジェクトの性質によって使い分ける。
テスト駆動開発(TDD)¶
テスト駆動開発(Test-Driven Development, TDD) は、実装コードより先にテストを書く開発手法。
Red-Green-Refactorサイクル¶
Red Green Refactor
┌───┐ ┌───┐ ┌───────┐
│失敗│ ───→ │成功│ ───→ │リファク│ ───→ (繰り返し)
│テスト│ │テスト│ │タリング│
└───┘ └───┘ └───────┘
↑ テストを先に書く ↑ 最小限の実装 ↑ 品質を高めるRed :まず失敗するテストを書く(まだ実装がないので当然失敗する)
Green :テストが通る最小限のコードを書く(動けばよい、きれいでなくていい)
Refactor :テストが通ったまま、コードをきれいにする
TDDのメリット¶
テストが設計ドキュメントになる(使う側から見た設計ができる)
実装後に「テストを書く気力がない」という問題が起きない
バグを早期に発見できる
変更時のリグレッションを防ぎやすい
TDDのデメリット・注意点¶
習熟に時間がかかる
UIや外部システムに強く依存するコードではテストが書きにくい
テストのための設計(依存性注入など)が必要になる
Pythonでの例¶
# 1. Red: 失敗するテストを書く
def test_fizzbuzz():
assert fizzbuzz(1) == "1"
assert fizzbuzz(3) == "Fizz"
assert fizzbuzz(5) == "Buzz"
assert fizzbuzz(15) == "FizzBuzz"
# この時点では fizzbuzz は未定義なのでテストは失敗する
# 2. Green: 最小限の実装を書く
def fizzbuzz(n: int) -> str:
if n % 15 == 0:
return "FizzBuzz"
if n % 3 == 0:
return "Fizz"
if n % 5 == 0:
return "Buzz"
return str(n)
# 3. Refactor: 必要に応じてきれいにする(今回はこのままでよい)リスクベーステスト¶
リスクベーステスト(Risk-Based Testing) は、テストリソースをリスクの高い箇所に集中させる戦略。すべてをテストすることは現実的に不可能なため、優先順位づけが重要になる。
リスクの評価軸¶
リスク = 発生確率 × 影響度
| 象限 | 発生確率 | 影響度 | 対応 |
|---|---|---|---|
| 高優先 | 高 | 高 | 念入りにテストする |
| 中優先(監視) | 低 | 高 | 少ないが確実なテスト |
| 中優先(効率化) | 高 | 低 | 自動化で効率的にカバー |
| 低優先 | 低 | 低 | テストを省略してもよい |
リスクの高い箇所の例¶
決済・認証などのセキュリティ・お金に関わる処理
データの書き込み・削除(取り消せない操作)
複雑なビジネスロジック(条件分岐が多い)
過去にバグが多かった箇所
外部サービスとの連携部分
多くのユーザーが利用する主要フロー
テスト優先順位のつけ方¶
ユーザーへの影響 :障害が起きたときにユーザーにどのくらい影響するか
修正コスト :本番で発見された場合の修正コストはどのくらいか
変更頻度 :よく変更されるコードは回帰リスクが高い
複雑さ :コードが複雑なほどバグが入り込みやすい
テスト自動化戦略¶
何を自動化すべきか¶
すべてのテストを自動化すべきではない。自動化に向いているものと、手動テストが向いているものがある。
| 自動化に向いている | 手動テストが向いている |
|---|---|
| 回帰テスト(毎回同じ確認) | 探索的テスト |
| ロジックの正確性確認 | ユーザビリティ確認 |
| 大量データの処理確認 | 新機能の初回確認 |
| CI/CDパイプラインでの品質ゲート | 視覚的なデザイン確認 |
テスト自動化の導入ステップ¶
スモークテストから始める :まず「動く」を確認する最小限のテストを自動化する
回帰テストを追加 :バグ修正のたびにテストを追加し、再発を防ぐ
主要フローをカバー :ユーザーが最も使うルートのE2Eテストを書く
ユニットテストで深める :ロジックが複雑な部分の単体テストを充実させる
CIとの連携¶
Pull Requestのたびに自動テストを実行する(品質ゲート)
main/masterへのマージ前にテストが全て通ることを必須にする
テスト実行時間が長くなりすぎないよう、並列化や対象の絞り込みを検討する
開発者がPRを作成
↓
CI が自動でテストを実行
↓
失敗? → 開発者が修正
↓
成功 → レビュー → マージテストの実行時間を管理する¶
テストが遅くなると開発体験が悪化し、テストをスキップするようになる。
単体テストは1件 < 1ms が目安
統合テストは数秒まで
E2Eテストは分単位も許容するが、本数を絞る
遅いテストは並列実行するか、別のパイプラインに分離する
テスト戦略の立て方¶
プロジェクトのテスト戦略を決めるとき、以下の観点を整理するとよい。
1. テストの目的を明確にする¶
何を守りたいのか(品質の定義)
どのリスクが最も大きいか(リスクの優先順位)
テストで検出できないことは何か(テストの限界)
2. テストの構成を決める¶
テストピラミッドやテストトロフィーを参考に、どの層にどれくらいのテストを置くかを決める。
システムの性質(バックエンド中心かUIが重要かなど)
開発チームの規模とスキル
CI/CDの実行時間の制約
3. テストの運用ルールを決める¶
いつテストを書くか(PRと同時? TDD?)
テストが失敗したときの対応フロー
Flaky testをどう扱うか
テストコードのレビュー方針
チェックリスト¶
主要なユーザーフローがE2Eまたは統合テストでカバーされているか
複雑なビジネスロジックに単体テストがあるか
バグ修正時にテストが追加されているか
CIでテストが自動実行されているか
Flaky testが放置されていないか
テスト実行時間は許容範囲内か