前置き
今回は、テストアーキテクチャおよびテスト設計における各原則間の
「トレードオフ」「相関関係」「因果関係」について、考えられるものを網羅的に解説していきたいと思います。
結局ここを避けてしまっては、金になるアーキテクチャなんてものは構築できません。
⚖️ テスト原則間のトレードオフ
ある原則を追求することが、別の原則の達成を困難にする場合の緊張関係です。
テストアーキテクチャを考える際には、このバランスも考える必要がありますし、
TDDを素直に採用している場合でも、プロダクションコードで満たしたい品質を考慮して、
このトレードオフと向き合う必要性があります。
1. 速度 vs 忠実性 (Speed vs Fidelity)
原則:FIRST原則 (Fast & Independent) vs テストピラミッドの頂点 (E2Eテスト)
理由
テストを高速(Fast)にするには、外部依存をモックやスタブで排除し、テスト対象を隔離(Isolated) する必要があります。
しかし隔離すればするほど、本番環境の振る舞いとの乖離(忠実性の低下)が生まれます。
一方、結合テストやE2Eテストのように忠実性を追求すると、実際のネットワークやDBなどの外部要因で失敗しやすく、独立性が低く・かつ本質的に低速なります。
解決策
テストピラミッドの戦略そのものが解決策になります。
両方を満たすことはできないため、バランスを取らないとダメです。
速度と隔離性を重視する単体テストを大量に書き
忠実性を重視する低速なE2Eテストは最小限に絞り、かつ実行頻度も最小限にする(例: 毎コミットではなく、夜間バッチで実行する)
ことで、両方のメリットをバランス良く満たします。
開発中の迅速なフィードバックは多数の独立した単体テストで得て、リリース前の最終的な信頼性は少数の忠実なE2Eテストで確保するという作戦です。
2. 可読性 vs DRY原則 (Readability vs Don't Repeat Yourself)
原則:AAAパターン vs DRY原則
理由
AAAパターンは、
各テストが何を準備(Arrange)し、何を実行(Act)し、何を検証(Assert)するか
が一読して分かる可読性を最優先します。
このため、複数のテストで似たようなセットアップコードが重複することが許容されます。
一方、DRY原則を過度に適用し、セットアップコードを複雑なヘルパー関数に共通化しすぎると、個々のテストの前提条件含むコンテキストが分かりにくくなり、可読性が損なわれる ことがあります。
そもそも、完ぺきにMECEなテスト設計なんて目指すのはナンセンスです。
解決策
テストコードにおいては、プロダクションコードほど厳格にDRYを適用せず、可読性を優先するのが一般的です。
多少の重複は、テストの意図を明確にするために許容してあげましょう。
3. 厳格さ vs コスト (Rigor vs Cost)
原則:ミューテーションテスト vs CI/CDの実行時間
理由
ミューテーションテストは、テストスイートの品質を最も厳格に評価する手法ですが、
コードの変異体(ミュータント)を大量に生成し、それぞれに対してテストを実行するため、非常に高い計算コストと時間を要します。
これは、迅速なフィードバックを重視するCI/CDの要求と明らかに対立します。
解決策
ミューテーションテストを全てのコードに毎回事前実行するのは非現実的なため、リスクベースのアプローチを取りましょう。
CI上のミューテーションテストの
実行頻度を下げて夜間のみ実行したり
特にバグが許されないクリティカルなモジュールに限定して適用
したりすることで、コストと厳格さのバランスを取ります。
※私のオススメは、事前の脅威モデリングによる、リスクシナリオを定性比較する方法です。それにより、もっともリスクポイントの高いものを特定しやすくなるからです。
4. 網羅性 vs 意味のある検証 (Coverage vs Meaningful Assertion)
原則:コードカバレッジ100% vs テストの本来の目的
理由
コードカバレッジは「コードがテスト中に実行されたか」を示すだけで、「その振る舞いが正しかったか」は保証しません。
カバレッジ100などの水準を目標にすると、assert文のない、ただコードを実行するだけの
無意味なテストが量産される危険性があります。
カバレッジは60%を下回るほど低すぎても問題ありですが、95%などどう考えても
完璧を目指し過ぎでしょ と感じるような水準でも問題があります。
解決策
カバレッジは目標ではなく、あくまで指標として使います。
テストが振る舞いを正しく検証しているかは、ピアレビューやミューテーションテストで評価します。
🤝 原則同士の相関関係
1つの原則を実践することが、別の原則の実践を助ける場合の関係についても触れましょう。
事前のモデリングなどによる、アーキテクチャ考案によって、質の高いプロダクションコードを設計でき、その結果、保守性の高いテストを定義しやすくなるということはよくあることです。
もちろん、卵が先か鶏が先かっていう感じです。
1. SOLID + FIRST + AAA → 保守性の高いテストコード
関係メカニズム
これらはテストコードの品質を高めるための三位一体です。
AAAパターンで構造化すると、自然と単一責任(S)を満たしやすくなります。
FIRSTの独立(I)を保つためには、依存性逆転(D)を適用してモックを使うことが有効。
これらが揃うことで、非常にクリーンで保守しやすいテストコードが生まれます。
2. CCP + SRP → 発見しやすいテスト構造
単一責任原則を満たしたアーキテクチャをモデリングで考察できているか?がキーです。
関係メカニズム
プロダクションコードが高凝集(CCPやSRPに従っている)であれば、テストコードも
「1プロダクションクラス 対 1テストクラス」
という対応関係を保ちやすくなります。
これにより、変更時にどのテストを修正すべきかという発見性が向上します。
3. 同値分割 + 境界値分析 → 効率的ブラックボックステスト
これらは補完関係にあるテストケース設計技法です。
関係メカニズム
まず同値分割で大まかなテストグループを定義し、次に境界値分析でそのグループの境界という最もバグが出やすい箇所をピンポイントで狙い撃ちすることで、最小限のテストケースで最大の効果を狙えます。
4. DIP (in Production Code) → 達成しやすいFIRST原則
プロダクションコードの安定依存な設計がテストの容易性を決めます。
関係メカニズム
プロダクションコードが依存性逆転の原則(DIP)に従い、抽象(インターフェース)に依存していれば、テストコードでモックやスタブを注入するのが非常に簡単になります。
これにより、
FIRST原則の独立(I)、再現可能(R)、高速(F)
が達成しやすくなります。
🏛️ 因果関係 (この原則を満たすには、この原則が必要)
ある原則が、別の原則を実践するための前提条件となる関係についても触れましょう。
以下のトピックでは、A→B という関係性は、
Bを満たすためには、Aが必要条件である
という関係式を表現しています。
テスト自体が技術的負債になったり、デリバリー速度を大幅に落とすことがないような、
質のいいテストを維持するためにも、絶対に以下のメカニズムを抑えておきましょう。
1. テストアーキテクチャ → 全ての原則
この関係性は、テストのアーキテクチャを考えるQAさんと、プロダクションコードの設計を行う人の認知の歪みがある限り、絶対に大なり小なり起こり得るものです。
たとえば、ドメインモデリング式の実装が求められているのに、テストはアイスクリームコーン状態であるとかが、まさにその代表例です。
関係性
最初に一貫したテストアーキテクチャ(戦略、ツール、環境)を定義しなければ、チーム内で各原則がバラバラに適用され、テストスイート全体として統制が取れなくなります。
アーキテクチャは、全ての原則を実践するための土台です。
2. SOLID原則 (特にDIP) → 効果的な単体テスト (FIRST)
ボブおじさんのクリーンアーキテクチャ本にも書かれているものですが、
テストコードが、プロダクションコードの具体に直接依存するような設計はNGです。
関係性
テスト対象のプロダクションコードがテストしやすいように設計されていることが、良い単体テストのための絶対的な前提条件です。
密結合な「大きな泥団子」のようなコードに対して、高速で独立した(FIRST)単体テストを書くことは不可能です。
3. FIRST原則 → 安全なリファクタリング
関係
マーティン・ファウラーが説くように、リファクタリングの最初のステップは
「堅牢なテストスイートを構築すること」
です。
高速で信頼性の高いテスト(FIRST原則を満たすテスト)という安全網がなければ、リファクタリングは「動いているコードを勘で修正する」という超危険な行為になってしまいます。
4. 境界値分析などのテスト設計技法 → 有効なミューテーションテスト
ミューテーションテストは、アジャイル開発の場合、1・2回目のスプリント期間中の、まだテスト自体が未成熟な頃では、意図的に盛り込まないという選択をした方が良いです。
関係
ミューテーションテストは、既存のテストスイートの品質を評価するものです。
評価対象となるテストスイートがなければ、そもそもなんも始まりません。
境界値分析などの技法を使って設計されたテストケース群があって初めて、ミューテーションテストはそのテスト群がどれだけプロダクションコードの変異を検出できるかを測定できます。