「テストを書いているのに、なぜか開発スピードが落ちている……」。そんな悩みを抱えていませんか。私も数年前までは、テストコードを書くこと自体が目的になっていました。カバレッジ100%を目指して、ひたすらモックを差し込む日々。しかし、コードを少し直すたびにテストが壊れ、修正に追われる地獄を味わいました。
結局、テストが「お荷物」になっていたのです。多くのエンジニアが陥るこの罠こそが、テストの本質を理解していない証拠です。質の低いテストを放置すると、リファクタリングができなくなり、システムは腐敗していきます。この記事では、名著『単体テストの考え方/使い方』の要点を凝縮して解説します。これを読めば、プロジェクトを救う「価値あるテスト」の書き方が分かります。
第1部: 単体(unit)テストとは?
第1章: なぜ、単体テストを行うのか?
単体テストを行う究極の目標は、プロジェクトの成長を持続可能なものにすることです。 コードベースは、放っておくと時間と共に「エントロピー(無秩序な量)」が増大し、複雑化して修復不能な状態へ向かいます。 変更を加えるたびにどこかが壊れる恐怖に怯え、開発スピードが鈍化していく……。 この劣化を防ぐセーフティネットこそが、質の高いテストスイートなのです。
ここで私たちが肝に銘じておくべきなのは、「コードもテストも、基本的には資産ではなく負債である」という冷徹な事実です。 テストコード自体も維持・メンテナンスにコストがかかります。 だからこそ、すべてのコードを平等にテストすれば良いわけではありません。 価値の低いテストを残すことは、ただプロジェクトの足を引っ張る「お荷物」を増やしているのと同じです。 最小限の保守コストで最大限の価値を生み出す。このドライな視点がプロの現場には不可欠です。
また、初心者が陥りがちなのが「網羅率(カバレッジ)の罠」です。 カバレッジが低いことはテスト不足のサインになりますが、逆に「高いから質が良い」という証明にはなりません。 分岐網羅率が100%であっても、肝心のライブラリの中身が検証対象外だったり、テスト自体が何も確認していなかったりするケースは多々あります。 数字に縛られすぎると、本当に重要な「システムの核」を守るための本質的な活動が疎かになります。 カバレッジを強制することは、むしろ開発の妨げになるとさえ警告しています。
優れたテストスイートには、明確な3つの特徴があります。
この「価値の良し悪し」を区別し、常にリファクタリングを繰り返してテストの質を高めていく。 それが、私たちが10年後も笑顔で開発を続けるための道です。
第2章: 単体テストとは何か?
エンジニアの間で「単体テスト」という言葉ほど、人によって定義がバラバラなものはありません。 ある人は「関数ごとのテスト」と言い、別の人は「クラスをモックで切り離したテスト」と主張します。 この認識のズレが、プロジェクト内での不毛な議論を生む原因です。 本書では、単体テストが備えるべき性質を以下の3点に集約しています。
この中でも特に「隔離」という言葉の解釈が、テストの運命を左右します。 実行速度についても、数秒ではなくミリ秒単位で終わるのが理想的。 開発中にストレスなく何度も回せる速度を維持しなければ、テストは次第に実行されなくなります。 結局、テストの定義を曖昧にしたまま書き始めると、メンテナンス不可能な「ゴミコードの山」を築くことになりかねません。
単体テストを定義する3つの要素
単体テストの3つの要素の中でも、特に議論の的になるのが「隔離」の意味です。 多くの人が「テスト対象のクラスを周囲から切り離すこと」だと勘違いしています。 しかし、本当の隔離とは「テスト・ケース同士が干渉しないこと」を指します。 テストを並列で実行しても、あるいは実行順序を入れ替えても、常に同じ結果が得られる状態こそが真の隔離です。
また、「1単位」をコードの1行や1クラスだと捉えるのも大きな間違い。 検証すべきはコードの構造ではなく、ユーザーにとって価値のある「振る舞い」です。 クラス単位でテストを細かく書きすぎると、リファクタリングでクラス構成を変えた瞬間にすべてのテストが壊れます。 私も以前、リファクタリングでクラスを分割した際、テストの修正に数日溶かした経験があります。 あれは今思い出しても、生産性の欠片もない虚しい時間でした。
ロンドン学派 vs. 古典学派:何が「単体」なのか?
単体テストの世界には、有名な2つの学派が存在します。 「ロンドン学派」は、テスト対象を他のクラスから完全に隔離すべきだと考えます。 そのため、依存するオブジェクトはすべてテスト・ダブル(モックなど)に置き換えるのが基本です。 一見すると「どこでバグが起きたか特定しやすい」というメリットがありますが、実はここに大きな罠が潜んでいます。
一方で、私が支持する「古典学派」は、クラスではなく「振る舞い」を隔離します。 不変な依存関係であれば実際のオブジェクトをそのまま使い、他のテストに影響を与える共有依存だけをモックにします。 古典学派のスタイルは、内部の実装に依存しないため、リファクタリングへの耐性が非常に高いのが特徴です。 ロンドン学派の聖典には『実践テスト駆動開発』がありますが、古典学派の根底にはケント・ベック氏の『テスト駆動開発』という揺るぎない哲学があります。
統合テストとE2Eテストの境界線
単体テストの性質を1つでも欠いたものは、すべて「統合テスト」に分類されます。 例えば、データベースなどのプロセス外依存をそのまま使ってテストを行う場合などがこれに当たります。 さらに、ユーザーの視点からシステム全体を検証する「E2Eテスト」も、統合テストの究極の形と言えます。 統合テストは、単体テストでは見つけられない「部品同士の結合の不備」を検知するために不可欠な存在です。
ただし、統合テストは実行速度が遅く、準備も大変だという欠点があります。 そのため、すべてのテストを統合テストで行うのは、保守コストの観点から見て合理的ではありません。 理想は、ビルド・プロセスの最後に実施し、個々の開発環境では単体テストをメインに回すという使い分けです。 時には境界線が曖昧になることもありますが、まずは自分のテストがどちらの性質に近いかを意識することが重要です。
第3章: 単体テストの構造的解析
テストコードが「読みにくい」と感じるなら、それは構造に問題があるかもしれません。良い単体テストを構築するには、まず全てのテストケースに共通の型を持たせることが重要です。
結論から述べると、全ての単体テストは「準備(Arrange)」「実行(Act)」「確認(Assert)」の3つのフェーズで構成される「AAAパターン」を適用すべきです。
この構造を徹底するだけで、テストの意図が明確になり、メンテナンスコストは劇的に下がります。私も昔は、1つのテストメソッドの中にダラダラと準備と実行を混ぜて書いていました。
しかし、それだと「何を検証したいのか」が分からなくなり、少しの変更でテストが壊れる原因になります。
AAAパターンは、オブジェクト指向プログラミングの「読みやすさ」を最大化する魔法の型です。テストを英語の物語のように記述できるようになれば、あなたはもう初心者卒業と言えるでしょう。
全テストの基本!AAAパターンの魔法
AAAパターンは、テストの構造を3つに明確に区別します。
まず「準備(Arrange)」で、テスト対象システム(SUT)とその依存関係を適切な状態にします。
次に「実行(Act)」で、検証したいメソッドを呼び出し、最後は「確認(Assert)」で結果が期待通りかを検証します。
このとき、各フェーズの間に1行の空行を入れるか、コメントで区切るのがプロの作法です。
実行フェーズのコードは、原則として1行に収めるべきです。
もし「実行」が複数行にわたるなら、それはSUTのAPI設計が「カプセル化」を壊しているサインかもしれません。
クライアントに複数のメソッド呼び出しを強制する設計は、不変条件の侵害を招くリスクがあります。
1単位の振る舞いとは?カプセル化を守る設計
単体テストで最も議論になるのが「1単位」の捉え方です。
クラスやメソッドといったコードの構造を「単位」にするのではなく、1つの「振る舞い」を検証の単位にするのが正解です。
1つのテストケースで複数の振る舞いを検証しようとすると、テストが肥大化し、失敗した際の原因特定が困難になります。
もし複数の振る舞いがあるなら、それは個別のテストケースに分割すべきだということです。
また、カプセル化を維持することは、テストのしやすさと設計の質を両立させる鍵となります。
実装の詳細は外部から隠蔽し、公開されたAPIを通じてのみ振る舞いを検証しなければなりません。
テストを通すためにプライベートな状態を公開するような誘惑に負けないでください。
それは「壊れやすいテスト」への第一歩であり、将来の自分に負債を残す行為に他なりません。
メンテナンス性を高める命名とフィクスチャの管理
テストの名前についても、厳格な命名規則に縛られる必要はありません。
むしろ、非開発者にも伝わるような「ドメイン領域の専門用語」を使った説明的な名前を付けるべきです。
英語で命名する場合でも、アンダースコア(_)を使って単語を区切り、読みやすさを最優先にしてください。
「テストメソッド名はメソッド名を含まなければならない」という思い込みは、今すぐ捨ててしまいましょう。
最後に、テストデータの準備に使う「テスト・フィクスチャ」の管理についてです。
共通の準備コードをコンストラクタに記述するのは、テスト間の結合を強めてしまうためお勧めしません。
代わりに「ファクトリメソッド」や「オブジェクト・マザー」パターンを活用しましょう。
これにより、各テストケースが必要なデータだけを明示的に準備できるようになり、コードの重複を避けつつ可読性を保てます。
Fluent Assertionsのようなライブラリを導入して、検証コードを自然な文章のように書くのも、非常に有効な手段です。
第2部: 単体テストとその価値
第4章: 良い単体テストを構成する4本の柱
世の中には「書かないほうがマシ」なテストが大量に存在します。では、何をもってテストの良し悪しを判断すればよいのでしょうか。本書では、良い単体テストを評価するための「4本の柱」を定義しています。
これらの柱は、単体テストだけでなく、統合テストやE2Eテストを分析する際にも役立つ万能な指標です。面白いのは、テストの価値はこれらの項目の「足し算」ではなく「掛け算」で決まるという点です。つまり、どれか1つでも0点があれば、そのテストの価値はゼロ、あるいはマイナスになってしまいます。
特に、プロジェクトが成長するにつれて「リファクタリングへの耐性」の重要性が増していきます。初期段階ではあまり気になりませんが、コードが複雑になるほど、偽陽性(バグがないのにテストが失敗すること)が開発の大きな足かせになるからです。価値のあるテストスイートを維持するには、これら4つのバランスを常に意識し、質の低いテストを容赦なく切り捨てる勇気が必要です。
退行に対する保護とリファクタリングへの耐性
「退行に対する保護」とは、コードを修正した際にバグをどれだけ正確に検知できるかという性質です。これは開発者が書いたコードだけでなく、利用しているライブラリやフレームワークのコードまで含めて評価する必要があります。この保護が強いほど、変更を加える際の安心感が高まります。
一方で「リファクタリングへの耐性」は、コードの内部構造を変えてもテストが壊れない能力を指します。ここで重要なのが「偽陽性」の防止です。偽陽性は、テストがプロダクションコードの内部実装と密結合しているときに発生します。頻繁に嘘の警告を出すテストは、開発者の信頼を失い、最終的には無視されるようになってしまいます。退行の保護とリファクタリングへの耐性は、テストの正確性を支える両輪なのです。
迅速なフィードバックと保守のしやすさ
テストは実行速度が命です。「迅速なフィードバック」は、コードに加えた変更が正しいかどうかを即座に教えてくれる性質です。どれだけ正確なテストでも、結果が出るまで数時間かかるようでは、開発サイクルの中に組み込むことはできません。単体テストが「ミリ秒単位」で終わるべきだと言われるのは、この迅速性を担保するためです。
「保守のしやすさ」は、テストケース自体の読みやすさと、実行環境の構築の難易度で決まります。テストコードが肥大化して理解が難しくなったり、データベースなどのプロセス外依存のセットアップが複雑すぎたりすると、保守コストが跳ね上がります。テストは「負債」の一種であることを忘れず、常に最小限のサイズで最大限の効果を狙う設計を心がけなければなりません。
価値のトレードオフとテスト・ピラミッド
4本の柱をすべて最大化できれば理想ですが、現実はそう甘くありません。実は「退行に対する保護」「リファクタリングへの耐性」「迅速なフィードバック」の3つは互いに相反する関係にあり、すべてを同時に満たすことは不可能です。そのため、テストの種類に応じてどこを重視するかを選択することになります。
ただし、「リファクタリングへの耐性」だけは、どんなテストであっても妥協してはいけません。これを捨ててしまうと、テストがただの重荷に変わるからです。また、各テストの特性を活かして配置する「テスト・ピラミッド」の考え方も重要です。単体テスト、統合テスト、E2Eテストを適切な比率で組み合わせることで、システム全体の信頼性を効率よく高められます。テストを作成する際はブラックボックス・テストとして振る舞いを確認し、分析する際だけホワイトボックス・テストとして詳細を確認するのが、賢いエンジニアのやり方です。
第5章: モックの利用とテストの壊れやすさ
「リファクタリングをしただけなのに、テストが真っ赤になった……」。そんな経験、あなたにもありませんか。私は数え切れないほどあります。この現象の正体は、テストが「実装の詳細」と密結合していることです。第5章では、テストの壊れやすさを回避するために不可欠な「モック」の正しい扱い方を学びます。
まず整理すべきは、テスト・ダブルの分類です。テスト・ダブルには「ダミー、スタブ、スパイ、モック、フェイク」の5種類がありますが、大きく2つのグループに分けられます。1つは、外部に向かうコミュニケーションを模倣・検証する「モック(モック、スパイ)」のグループ。もう1つは、内部に向かうコミュニケーションを模倣する「スタブ(ダミー、スタブ、フェイク)」のグループです。
この違いを理解せずにモック化を乱用すると、テストは一気に「壊れやすい負債」へと成り下がります。特にスタブとのやり取りを検証(アサート)してしまうのは、テストを壊れやすくする最大の原因です。スタブからのデータ取得は、最終的な結果を生み出すための中間ステップに過ぎません。つまり、実装の詳細をテストしているのと同じなのです。良いテストを書くためには、モックとスタブを明確に使い分ける「審美眼」を養う必要があります。
モックとスタブの決定的な違いとは?
モックとスタブを使い分ける鍵は、コミュニケーションの「方向」にあります。モックは、テスト対象システムから「外部」に向かう呼び出しを模倣するために使います。例えば、メール送信や外部APIの呼び出しなど、システムが外部に与える副作用を確認する場合です。これは「コマンド」に相当し、検証の対象としても安全です。
対して、スタブは外部からシステムへ「内部」に向かうデータの取得を模倣します。データベースからのクエリ結果などがこれに当たります。スタブとのやり取りを検証することは、脳内の神経細胞の反応を一つひとつチェックするようなもので、あまりに粒度が細かすぎます。重要なのは「その人が助けてくれたか」という結果であって、脳のどの回路が動いたかではないはずです。スタブを検証対象に含めるのは、今日からすぐにでもやめましょう。
観察可能な振る舞い vs 実装の詳細
テストがリファクタリングに耐えられるかどうかは、何を検証しているかで決まります。本書では「観察可能な振る舞い」と「実装の詳細」を厳格に区別しています。観察可能な振る舞いとは、クライアントが目標を達成するために公開された「操作」や「状態」のこと。それ以外のコードはすべて「実装の詳細」です。
きちんと設計されたコードは、公開されたAPIと観察可能な振る舞いが一致しています。もし、実装の詳細が不当に公開されてしまうと、テストがその「漏れた詳細」に依存してしまいます。その結果、内部を少し変えただけでテストが失敗するようになるわけです。カプセル化を維持し、不変条件を守るための手段を外部から操作できないように隠蔽すること。これが、堅牢なシステムと壊れないテストを作るための鉄則です。
ヘキサゴナル・アーキテクチャと通信の分類
システムの構造を「ヘキサゴナル・アーキテクチャ」の視点で見ると、モックを使うべき場所がより明確になります。このアーキテクチャは、ビジネスロジックを担う「ドメイン層」と、外部との連携を指揮する「アプリケーション・サービス層」を分離します。ここで重要なのは、通信には「システム内」と「システム間」の2種類があるという点です。
システム内コミュニケーションは、ほとんどの場合「実装の詳細」に当たります。ここにモックを使うと、テストは極めて壊れやすくなります。一方、システム間コミュニケーションは、システムの境界を越える副作用を伴うため「観察可能な振る舞い」となります。モックを使ってよいのは、この「システム間」の通信であり、かつその副作用が外部から観察できる場合だけです。このルールを守るだけで、あなたのテストスイートの信頼性は劇的に向上するでしょう。
第6章: 単体テストの3つの手法
単体テストをどのように書くべきかという問いに対し、本書では3つの手法を提示しています。具体的には「出力値ベース・テスト」「状態ベース・テスト」「コミュニケーション・ベース・テスト」の3種類です。結論から言うと、私たちが目指すべきは「出力値ベース・テスト」を可能な限り増やすことです。なぜなら、この手法が最も保守性が高く、リファクタリングへの耐性が強いから。
多くの現場では、これら3つの手法が無意識に混在しているのが実情です。しかし、それぞれの特性を理解せずにテストを書くと、意図せず「壊れやすいテスト」を量産してしまいます。古典学派は状態ベースを好み、ロンドン学派はコミュニケーション・ベースを好む傾向にありますが、どの学派であっても出力値ベースが最強であるという事実は揺らぎません。実装の詳細と結び付きにくいテストをいかに作るか。そのための戦略が、この章には凝縮されています。
手法1:最も保守性が高い「出力値ベース・テスト」
出力値ベース・テストは、テスト対象に入力を与え、戻り値を検証する極めてシンプルな手法です。この手法の前提は、隠れた入力や出力を持たない「数学的関数」であること。内部の状態を一切気にせず、結果だけを見るため、リファクタリングで中身をどう書き換えてもテストが壊れません。テストコード自体も短く簡潔になるため、読みやすさと保守性の両面で圧倒的な優位性を持っています。
手法2:オブジェクトの変化を追う「状態ベース・テスト」
状態ベース・テストは、実行後のオブジェクトや協力者オブジェクトの状態を検証する手法です。出力値ベースに比べると、テスト対象の「状態」を公開する必要があるため、カプセル化が崩れやすく、テストコードも肥大化しがちです。保守性を高めるには、ヘルパーメソッドなどを使ってテストコードの可読性を上げる工夫が欠かせません。それでも出力値ベースよりは壊れやすくなるため、注意深く扱う必要があります。
手法3:依存先とのやり取りを見る「コミュニケーション・ベース・テスト」
モックを使って依存先とのやり取りを検証するのが、コミュニケーション・ベース・テストです。これは3つの手法の中で最も保守性が低く、扱いにくい手法と言えます。モックを多用すると、どうしてもテストが実装の詳細と密結合してしまうからです。第5章でも触れた通り、アプリケーションの境界を越える副作用を確認する場合など、限定的な場面で使うのが賢明な判断です。
関数型アーキテクチャによるテストの純粋化
出力値ベース・テストを増やすための強力な武器が、関数型プログラミングの考え方を取り入れた「関数型アーキテクチャ」です。この設計では、ビジネスロジックを担う純粋な「関数的核」と、副作用を扱う「可変の外殻」を明確に分離します。ロジックを数学的関数として独立させれば、テストのほとんどを出力値ベースで書けるようになります。
ただし、このアーキテクチャの導入は、保守性とパフォーマンスのトレードオフになることもあります。すべてのプロジェクトに適しているわけではありませんが、複雑なロジックをテストしやすくするには非常に有効な手段です。大切なのは、闇雲に手法を選ぶのではなく、システムの複雑さに応じて戦略的にテストを設計すること。これが、プロのエンジニアに求められるバランス感覚です。
第7章: 単体テストの価値を高めるリファクタリング
テストを書きやすくするためには、まずコード自体を「テストしやすい形」に整理しなければなりません。
結論から述べると、すべてのプロダクションコードを「複雑さ・重要性」と「協力者オブジェクトの数」という2つの軸で4種類に分類し、それぞれに最適なテスト戦略を採るべきです。
なぜなら、すべてのコードに対して同じ熱量でテストを書こうとすると、保守コストばかりが膨らみ、肝心のバグを見逃す「質の低いテストスイート」が出来上がってしまうからです。
コードの複雑さは条件分岐の数で決まり、ドメインにおける重要性は「そのコードがビジネス上の問題を解決しているか」で決まります。
この指標に基づくと、コードは以下の4つに分類可能です。
私も以前、何でもかんでも一つのクラスに詰め込んで、巨大な「神クラス」を作ってしまったことがあります。
そのクラスのテストを書こうとすると、大量のモックを準備しなければならず、1つのテストケースを書くのに半日かかることも珍しくありませんでした。
しかし、本書の教えに従ってロジックをドメイン層へ、連携をコントローラーへと分離したところ、テストコードが驚くほどスッキリしました。
優れた設計は、テストを「書かなければならない苦行」から「自然に書ける資産」へと変えてくれるのです。
質素なオブジェクト(Humble Object)と設計のトレードオフ
過度に複雑なコードを打破する強力な武器が「質素なオブジェクト(Humble Object)」パターンです。
これは、依存関係が多くてテストしにくいコードから、ビジネスロジックだけを別のクラスに抽出する手法を指します。
残された元のクラスは、単にロジックを呼び出すだけの「質素な」存在(コントローラー)になり、抽出されたクラスは純粋なドメインモデルとして簡単にテストできるようになります。
ただし、この分離を行う際には、常に「3つの性質」のトレードオフに直面することを覚悟してください。
その3つとは、「ドメインモデルのテストのしやすさ」「コントローラーの簡潔さ」「パフォーマンスの高さ」です。
残念ながら、これらすべてを同時に最大化することは不可能です。
例えば、外部依存へのアクセスをすべてコントローラーの最初と最後にまとめれば、ロジックのテストはしやすくなりますが、不要なデータの読み込みが発生してパフォーマンスが落ちるかもしれません。
このジレンマを解決するための具体的なパターンとして「確認後実行(CanExecute/Execute)パターン」や「ドメイン・イベント」があります。
確認後実行パターンを使えば、実行前に前提条件をチェックする責任をドメインモデルに持たせつつ、コントローラーの意思決定をシンプルに保てます。
また、ドメインモデル内で発生した状態変化を「イベント」として記録し、それを後で外部プロセスへ通知するようにすれば、ドメイン層を汚さずに副作用を扱えます。
どの性質を優先するかはプロジェクトの特性によりますが、迷ったら「リフレッシュしても壊れないテストのしやすさ」を優先するのが、長期的には賢い選択だと言えるでしょう。
第3部: 統合(integration)テスト
第8章: なぜ、統合(integration)テストを行うのか?
単体テストが「個別の部品」の正しさを保証するのに対し、それらを繋ぎ合わせて「システム全体」として機能するかを確認するのが統合テストの役割です。
統合テストは、単体テストでは検証できないプロセス外依存(データベースやメッセージ・バスなど)との連携を網羅します。
私も経験がありますが、単体テストがすべてパスしているのに、いざ動かしてみるとデータベースの型不一致で落ちる……なんてことは「エンジニアあるある」ですよね(笑)。
統合テストは単体テストに比べて、退行に対する保護やリファクタリングへの耐性が優れています。
一方で、実行スピードが遅く、保守コストが高くなるというデメリットも抱えています。
そのため、テスト・ピラミッドの原則に従い、大半のケースは単体テストで検証し、主要なパスや複雑な連携を統合テストで補完する戦略が最も効率的です。
また、システムの信頼性を高めるために「早期失敗(Fail Fast)原則」を取り入れることも重要です。
バグが発生した瞬間に処理を失敗させることで、問題の切り分けが容易になり、不整合なデータが蓄積されるのを防げます。
統合テストを正しく配置することで、開発チームは「このコードをデプロイしても大丈夫だ」という強い自信を持てるようになります。
管理下にある依存 vs 管理下にない依存:モックの使い分け
統合テストを設計する上で最も重要なのが、依存関係を「管理下にあるもの」と「管理下にないもの」に分けることです。
管理下にある依存とは、自分のアプリケーションからしかアクセスできないリソース(専用のデータベースなど)を指します。
これらとのやり取りは「実装の詳細」に分類されるため、テストではモックを使わず、実際の実装を使って検証すべきです。
一方、管理下にない依存とは、他のアプリケーションからもアクセスされる外部システム(メールサービスやメッセージ・パスなど)を指します。
これらとのコミュニケーションは「観察可能な振る舞い」の一部となるため、テストではモックを使用して、期待通りのメッセージが送信されているかを確認します。
この区別を曖昧にして、何でもかんでもモックに置き換えてしまうと、テストの信頼性が一気に損なわれるので注意が必要です。
「不必要なインターフェース」という負債
Javaエンジニアの皆さんは、クラスを作るときに「とりあえずInterface」を作っていませんか。
実は、実装クラスを1つしか持たないインターフェースを無闇に作るのは、YAGNI(You Aren't Gonna Need It)原則に反する設計上の負債です。
インターフェースが正当化されるのは、複数の実装が存在する場合や、管理下にない依存をモック化するために抽象化が必要な場合だけです。
管理下にある依存(自前DBなど)に対してインターフェースを導入し、モックでテストするのは「偽陽性」を生む原因になります。
ドメイン・クラスとコントローラーの境界が明確であれば、直接クラスを指定してテストを書くほうが、コードの意図が伝わりやすくなります。
間接参照の層を増やしすぎると、コードの理解を妨げる「ノイズ」になってしまうため、ドメイン、アプリケーション、インフラの3層程度に留めるのがベストです。
ログの検証:診断用ログはテストしない
意外と議論になるのが「ログをテストすべきか」という問題です。
本書の答えは明確で、サポートスタッフや管理者が目にする「サポート・ログ」はビジネス要求の一部としてテストすべきですが、開発者用の「診断用ログ」はテストすべきではありません。
診断用ログは実装の詳細に過ぎず、これをテストに含めると、ログメッセージを少し変えただけでテストが壊れてしまいます。
ビジネス上の重要な出来事を追跡したい場合は、ドメイン・イベントを利用してログを出力する DomainLogger などのクラスを導入しましょう。
また、循環依存はコードを理解しづらくする元凶ですので、バリュー・オブジェクトを導入するなどして構造を整理してください。
すべての依存関係をコンストラクタ経由で明示的に注入するように設計すれば、統合テストの準備もぐっと楽になります。
第9章: モックのベスト・プラクティス
モックは、管理下にない依存関係とのコミュニケーションを検証する場合にのみ使用してください。これが、テストを壊れにくくするための絶対的なルールです。管理下にある依存(自前で制御できるデータベースなど)をモックにしてしまうと、テストが内部の実装と密結合し、リファクタリングの耐性が失われます。モック化の対象は、システムの境界に位置する最後のコンポーネントに限定すべきです。
なぜシステムの境界でモックを使うのか。それは、統合テストによってより多くのコードが検証されるようになり、リファクタリングへの耐性が高まるからです。ドメインクラスの内部でモックをこねくり回すのではなく、コントローラーなどの境界部分で外部への出力を差し替えるのが、プロのやりかたです。また、モックは統合テストでのみ使用し、単体テストでは原則として使わないという指針も重要になります。
管理下にない依存だけをモックにする
管理下にない依存とは、メールサービスやメッセージ・バスなど、他のアプリケーションからも参照される外部リソースを指します。これらとのやり取りは「観察可能な振る舞い」そのものであるため、モックを使って正しい形式でメッセージが送られているかを確認する必要があります。
一方で、自分たちのアプリケーションしか関与しないデータベースなどは、管理下にある依存です。これらをモックに置き換えて検証することは、実装の詳細をテストしているのと同じ。将来的にテーブル構造を変えた際、プロダクションコードは正しいのにテストだけが失敗するという「偽陽性」を招く元凶になります。
システムの境界でモックを入れ替える
モックを適用する場所は、アプリケーションの最も外側の層であるコントローラーにすべきです。ドメインモデルの奥深くでモックを差し込むと、コードの構造が変わるたびにテストコードを修正しなければならなくなります。
コントローラーから外部へ向かう流れの中で、最後のコンポーネントをモックに置き換える。そうすることで、ビジネスロジック全体を通した検証が可能になり、テストの価値が最大化されます。複数のモックを1つのテストケースで使っても問題ありません。検証すべき外部システムが3つあれば、3つのモックを使えばいいだけです。
スパイと手書きのモックを活用する
モックライブラリに頼りすぎるのも考えものです。実は、自分自身で作成した「スパイ」と呼ばれる手書きのモックのほうが、可読性と保守性に優れているケースが多くあります。スパイはシステムの境界にあるクラスを代役として実装したもので、テストコードが非常に簡潔になります。
ライブラリを使うと設定コードが肥大化しがちですが、自前のスパイなら必要な検証コードをそこに集約できます。複数のテストで同じコードを呼び出すようにすれば、テストスイート全体のコード量を減らせるというメリットもあります。JavaならMockitoなどの有名どころがありますが、あえて「手書き」という選択肢を忘れないでください。
同義語反復(tautology)を避ける
テストを書く際、プロダクションコードにある定数やリテラルをそのまま使いまわしてはいけません。これは「同義語反復(tautology)」と呼ばれるアンチパターンです。例えば、エラーメッセージの内容をプロダクションコードの定数から参照して検証すると、定数の値自体が間違っていてもテストがパスしてしまいます。
テストはプロダクションコードから完全に独立している必要があります。検証に使う値は、あえてテストコード内に直接記述するか、必要に応じて複製してください。そうしないと、実質的に何も検証していない、無意味なテストケースを作成することになります。
サードパーティ製ライブラリの扱い
自分が所有していないコードを直接モックにしてはいけません。外部ライブラリの型に対してモックを作成すると、ライブラリのバージョンアップで型が変わった瞬間に、すべてのテストが動作しなくなります。
正しいやりかたは、ライブラリをラップする独自の「アダプタ」を作成することです。そして、そのアダプタに対してモックを作成するようにします。これにより、ライブラリの詳細からテストを保護しつつ、自分たちのビジネスドメインに適したインターフェースで検証が行えるようになります。
第10章: データベースに対するテスト
データベースに対するテストは、統合テストの中でも最も重要で、かつ最も厄介な存在です。
結論から述べると、データベースは「モック」にせず、本番環境と同じ種類のデータベースを使用してテストを行うべきです。
なぜなら、データベースとのやり取りをモックにしてしまうと、リファクタリングへの耐性が著しく損なわれるからです。
私も昔は、実行速度を優先して「SQLiteのインメモリモード」でテストを書いていた時期がありました。
しかし、本番のPostgreSQL独自の関数や制約で落ちるバグを何度も見逃し、結局は本物のDBでテストし直す羽目になりました。
データベースは、アプリケーションからのみアクセスされる「管理下にある依存」であることが多いです。
このような依存先とのやり取りは「実装の詳細」に過ぎないため、モックを使わずに実際のインスタンスを使って検証するのが鉄則。
もしO/Rマッパーを使っているなら、DBのベンダーを変えたときやスキーマを変更したときに、このテストが強力な保護となってあなたを救ってくれます。
スキーマ管理はGitで行い、移行ベースを選択する
データベースの構造、つまりスキーマは、ソースコードと同じようにGitなどのバージョン管理システムで管理されるようにしてください。
これには、テーブル、ビュー、インデックスはもちろん、ストアド・プロシージャや初期データである「参照データ」も含まれます。
参照データとは、アプリケーションが適切に機能するために事前に用意しておかなければならないデータのこと。
これらをコードと一緒に管理することで、どの開発者の環境でも、あるいはCI環境でも、常に同じ構成のDBを再現できるようになります。
また、データベースの変更を反映する方法には「状態ベース」と「移行ベース」の2つがありますが、本書では「移行ベース」を強く推奨しています。
移行ベースは、変更内容をSQLスクリプトとして順次適用していく方式で、DBがどのような経緯で現在の状態になったのかを明確にできます。
状態ベースは比較ツールに頼り切りになりがちで、複雑なデータの移動が伴う場合に意図しない挙動を生むリスクがあるため、避けるのが賢明です。
テスト環境の構築:開発者ごとに個別のインスタンスを
開発効率を最大化するためには、各開発者が自分自身のマシン上で個別のデータベース・インスタンスを持てるようにすべきです。
Dockerなどのコンテナ技術を使えば、この環境構築は非常に簡単になりましたよね。
共有のデータベースを複数の開発者で使うのは、絶対に避けてください。
ある開発者のテストが別の開発者のテストデータと干渉して、ランダムにテストが失敗する……なんて状況は、エンジニアの精神衛生上よろしくありません。
テスト・ケースは、原則として1つずつ順番に実行するようにしてください。
複数のテストを同時に並列実行しようとすると、DBの状態が混ざり合って、結果が不安定になるからです。
並列化による速度向上よりも、テストの「信頼性」を優先するのがプロの選択。
もしどうしても並列化したいなら、テスト・ケースごとに独立したDBインスタンスやスキーマを用意するしかありませんが、その保守コストに見合う成果が得られることは滅多にありません。
テスト・データのセットアップと後始末の極意
テスト・ケースの中で実行(Act)フェーズに進む前には、必ずDBをクリーンな状態にする必要があります。
ここで大切なのは、データの掃除をテストの「最後」ではなく「開始時」に行うことです。
テストの最後に掃除をしようとすると、テストが途中でクラッシュした場合に残骸が残り、次のテストに悪影響を与えてしまいます。
開始時に掃除をすれば、前のテストがどう終わっていようが確実な状態でスタートできるため、テストの堅牢性がぐっと上がります。
準備フェーズでは、コード量を減らすために「オブジェクト・マザー」などのパターンを活用しましょう。
また、実行フェーズではコントローラーのメソッド呼び出しを委譲するメソッドを用意し、確認フェーズでは「流れるようなインターフェース」を導入すると、テストが格段に読みやすくなります。
なお、各フェーズ(準備、実行、確認)で同じDBトランザクションを使い回してはいけません。
各フェーズごとに独立したトランザクションを使うことで、本番環境に近い、より正確な検証が可能になります。
リポジトリを単体でテストしてはいけない理由
最後によくある間違いを1つ。リポジトリ・クラス(DBへのアクセスを担うクラス)を個別に単体テストするのはやめましょう。
リポジトリだけをテストしても、リファクタリングへの耐性はほとんど得られない一方で、複雑なモックの仕組みを維持するための膨大なコストがかかってしまいます。
同様に、EventDispatcherクラスなどを個別にテストするのもコスパが悪いです。
リポジトリの検証は、それを利用するコントローラーなどの「統合テストのシナリオの一部」として含めるのが正解です。
アプリケーションが目的を達成する一連の流れの中で、DBに正しくデータが保存されたかを確認する。
この視点を持つだけで、テスト・コードの量は劇的に減り、かつバグに対する保護はより強力なものになります。
テストはあくまで「目的」ではなく「手段」であることを忘れずに、価値あるテストだけを積み上げていきましょう。
第4部: 単体テストのアンチ・パターン
第11章: 単体テストのアンチ・パターン
単体テストの道を極めようとすると、誰もが一度は「良かれと思ってやったことが裏目に出る」という経験をします。
テストを書くこと自体に熱中しすぎると、いつの間にかテストがプロダクション・コードの自由を奪い、開発の足を引っ張る「鎖」に変わってしまうのです。
この第11章では、私たちが陥りがちなアンチ・パターンを徹底的に洗い出し、どう回避すべきかを考えます。
特に、テストのためにコードの「カプセル化」を壊すような行為は、長期的なメンテナンス性を著しく低下させます。
実装の詳細をテストに晒せば晒すほど、コードを綺麗にするためのリファクタリングが困難になるからです。
私も以前、テストを通すためだけにプライベート変数をパブリックにしたことがありますが、あれはまさに「負債の種」をまく行為でした。
プロのエンジニアとして、テストの「質」を見極めるための最後の仕上げをしていきましょう。
プライベート・メソッドをテストしてはいけない理由
「この複雑なロジック、プライベート・メソッドだけどテストしたい……」と思ったことはありませんか。
結論を言うと、単体テストを行えるようにするという目的だけで、プライベート・メソッドを公開してはいけません。
プライベート・メソッドを直接テストすることは、テストを実装に密結合させ、リファクタリングへの耐性を失わせることに直結するからです。
もし、プライベート・メソッドを検証しないと安心できないほどそのロジックが複雑なら、それは設計に「抽象化の欠落」があるサインです。
その場合は、メソッドを公開するのではなく、ロジックを別の新しいクラスとして抽出し、その新しいクラスのパブリックな振る舞いとしてテストすべきです。
プライベート・メソッドは、あくまで観察可能な振る舞いの一部として、間接的に検証される形に留めるのが健全な設計と言えます。
ブラック・ボックス・テストとホワイト・ボックス・テストの使い分け
テストには、中身を知らずに振る舞いを見る「ブラック・ボックス・テスト」と、内部構造を知った上で行う「ホワイト・ボックス・テスト」があります。
ここでの鉄則は、テストを作成する際は「ブラック・ボックス・テスト」として扱い、テストを分析する際(網羅率の確認など)にのみ「ホワイト・ボックス・テスト」の手法を用いることです。
プロダクション・コードのアルゴリズムやロジックの詳細な知識をテストに持ち込むと、ドメイン知識がテストに漏洩し、保守コストが増大します。
また、単体テストを行えるようにするために、プライベートにすべき「状態」を公開するのも厳禁です。
「テストだから特別に」という甘えを許すと、プロダクション・コードはたちまち汚染されていきます。
テストコードは、あくまで外部のクライアントと同じ立場で、公開されたAPIを通じてのみシステムと対話するべきなのです。
プロダクション・コードへの汚染と不自然なテスト・ダブル
テストのためだけにプロダクション・コードに特別なロジックを追加することを「コードへの汚染」と呼びます。
例えば、「もしテスト実行中ならこの処理をスキップする」といったフラグを埋め込むような行為です。
これはアンチ・パターンの典型であり、テストの信頼性を損なうだけでなく、本番環境での予期せぬ挙動を引き起こすリスクになります。
具象クラスのテスト・ダブル(モックなど)を作成することも避けるべきです。
既存の機能をそのまま使い回すために具象クラスを継承して代役を作るのは、単一責任の原則に反している可能性が高いからです。
このような場合は、ドメイン・ロジックを扱うクラスとプロセス外依存を扱うクラスの2つに適切に分離するリファクタリングを検討してください。
現在日時の扱い:環境コンテキストの罠
現在日時をコード内で直接参照する「環境コンテキスト」も、テストを難しくする要因です。
日時に依存したテストは実行するタイミングによって結果が変わるため、非決定的で不安定なテストになってしまいます。
これを解決するために、日時は明示的に依存として注入させるようにしてください。
注入の方法には「サービスとして注入する(Clockなどのインターフェースを使う)」方法と「値として注入する」方法の2つがありますが、可能な限り「値として注入する」方法を選択してください。
値を直接渡すほうが、テストの準備が簡単になり、コードの意図もより明確になります。
こうした細かな設計の積み重ねが、1,000個、2,000個とテストが増えていったときの「壊れにくさ」の差となって現れてくるのです。
まとめ:価値あるテストが開発を楽しくする
テストは、私たちエンジニアを守ってくれる盾であるべきです。しかし、考え方を間違えると、自分たちの足を引っ張る鎖になってしまいます。大切なのは、カバレッジという数字に惑わされず「そのテストに価値があるか」を常に問い続けること。
本書の教えを実践すれば、リファクタリングを恐れる必要がなくなります。コードを綺麗に保ち、自信を持ってデプロイできる。そんな当たり前だけど難しい理想の状態を、ぜひ手に入れてください。私もまだまだ修行中ですが、一緒に「一生壊れないテスト」を目指して頑張りましょう。