MENU

単体テストの考え方/使い方 まとめ

この書籍は、単に「テストの書き方」を教えるものではなく、「テストの価値(Value)を最大化し、メンテナンスコストを最小化するにはどうすればよいか」という、エンジニアリングの根本的な問いに答える名著です。TDD(テスト駆動開発)のドグマにとらわれず、実用的なアプローチを重視しています。

単体(unit)テストとは

目次

第1章:単体テストの目標(The Goal of Unit Testing)

多くのエンジニアが「バグを見つけるため」や「要件を満たすため」にテストを書きますが、著者はより高い視点での目標を提示しています。

1.1 本当の目標は「持続可能性」

単体テストの第一の目標は、ソフトウェアプロジェクトの持続可能な成長を可能にすることです。

ソフトウェア開発において、初期段階ではスピードが出ますが、時間が経つにつれてコードベースが複雑化し(エントロピーの増大)、開発速度は劇的に低下します。テストはこの「速度低下」を防ぐための安全網です。

バグを見つけるのは単体テストの「目標」ではなく、あくまで「副作用(副次的なメリット)」にすぎない。

開発速度とテストの関係

graph LR
    subgraph "テストなし"
    A[初期: 爆速] --> B[中期: バグ修正に追われる]
    B --> C[後期: 変更が怖くて動けない]
    end
    
    subgraph "適切なテストあり"
    D[初期: 少しコストがかかる] --> E[中期: 安定した速度]
    E --> F[後期: 恐れずリファクタリング可能]
    end
    
    style C fill:#f9f,stroke:#333
    style F fill:#9f9,stroke:#333

1.2 良いテストと悪いテストのコスト

すべてのテストが資産になるわけではありません。「悪いテスト」は負債になります。

  • 良いテスト: リファクタリングを助け、変更時の回帰(リグレッション)を防ぐ。
  • 悪いテスト: コードを変更するたびに壊れ、修正が必要になる(False Positive)。これは開発速度をむしろ低下させます。

1.3 カバレッジの罠

「カバレッジ率」を目標にすることは危険です。

指標説明注意点
コードカバレッジ実行された行数 / 全行数1行に圧縮すればカバレッジは変わるため、品質を保証しない。
ブランチカバレッジ実行された分岐数 / 全分岐数コードカバレッジよりはマシだが、検証(Assertion)がないテストでも100%になり得る。

特定のカバレッジ数字を目標にすると、エンジニアは「意味のないテスト」を書いて数字だけを満たそうとする(グッドハートの法則)。カバレッジはあくまで「テストされていないコードを見つけるための指標」であり、品質のターゲットにしてはいけない。

1.4 統合テスト(Integration Tests)とは何か

著者は統合テストを「単体テストではないもの」と消極的に定義しています。第2章の「単体テストの3つの要件」を思い出してください。

  1. 単一の動作単位を検証する
  2. 実行時間が短い
  3. 隔離された状態で実行される

この3つのうち、**1つでも満たさないテストはすべて「統合テスト」に分類されます。 具体的には、「共有依存(データベース、ファイルシステムなど)にアクセスするテスト」**は、隔離されていない(他テストと状態を共有する)ため、統合テストとなります。

1.5 テストピラミッド(The Test Pyramid)

単体テスト、統合テスト、E2Eテストのバランスをどう取るかについての指針です。test automation pyramidの画像

Getty Images

graph TD
    A[E2Eテスト<br>遅い・高コスト・信頼性最高]
    B[統合テスト<br>中速・共有依存を含む]
    C[単体テスト<br>高速・低コスト・基盤]
    
    A --- B
    B --- C
    
    style A fill:#ff9999,stroke:#333
    style B fill:#ffff99,stroke:#333
    style C fill:#99ff99,stroke:#333
  • 単体テスト(底層): テスト全体の大部分を占めるべきです。安価で高速だからです。
  • 統合テスト(中層): 単体テストではカバーできない「共有依存との連携」を確認します。
  • E2Eテスト(頂点): ユーザーの挙動を模倣します。最も現実に近いですが、維持コストが高いため、重要なフローに絞るべきです。

エンジニアへの教訓: すべてを統合テストやE2Eでカバーしようとしてはいけない。実行時間が長くなりすぎ、フィードバックループが遅くなるため、結局テストが実行されなくなる。


第2章:単体テストとは何か(What is a Unit Test?)

「単体テスト」の定義は人によって異なりますが、著者は以下の3つの属性を満たすものと定義しています。

  1. **単一の動作単位(Unit of behavior)**を検証する。
  2. 実行時間が短い
  3. 隔離(Isolation)された状態で実行される。

この「隔離」の解釈の違いにより、単体テストには大きく2つの派閥(学派)が存在します。これが本章のハイライトです。

2.1 古典派 vs ロンドン派

ここでの理解が、後の章での「モックの使い方」に直結します。

特徴ロンドン派(London School)別名: モック主義者古典派(Classical School)別名: デトロイト派
隔離の定義**テスト対象クラス(SUT)**を他から隔離する。依存関係はすべてモックにする。テストケース自体を隔離する。テスト同士が影響し合わなければ、実物のクラスを使っても良い。
単位(Unit)クラス(Class)振る舞い(Behavior)
依存の扱い不変オブジェクト以外はすべてモック化。共有依存(DBやファイルシステムなど)のみモック化。それ以外は実物を使う。
メリットテストが細粒度になり、問題箇所の特定が容易。失敗したときにどこが悪いかすぐわかる。過剰なモック(仕様への結合)を防ぎ、リファクタリングに強い。実運用に近い信頼性が高い。
デメリット実装の詳細に結合しやすく、リファクタリングでテストが壊れやすい。依存関係が複雑だとセットアップが大変になることがある。

2.2 著者の推奨:古典派(Classical School)

本書では古典派のアプローチを推奨しています。

理由は、ロンドン派のアプローチ(過度なモック化)は、クラスの実装詳細にテストが依存してしまい、リファクタリング耐性が低くなるからです。

「共有依存(Shared Dependency)」と「プライベート依存(Private Dependency)」を区別せよ。

  • 共有依存: テスト間で共有され、互いに影響を及ぼしうるもの(例: 書き込み可能なデータベース、ファイルシステム、静的フィールド)。これはモック化(スタブ化)すべき
  • プライベート依存: そのテスト内でのみ完結するもの。たとえ複雑なクラスであっても、共有されないなら実物を使うべき
flowchart TD
    Dependency["依存関係"]
    
    Dependency --> Shared["共有依存<br>(DB, 外部API)"]
    Dependency --> Private["プライベート依存"]
    
    Shared --> Mock["モック/スタブ化する"]
    
    Private --> Mutable["可変依存<br>(通常のクラス)"]
    Private --> Value["値オブジェクト<br>(不変)"]
    
    Mutable --> Real["実物を使う<br>(古典派推奨)"]
    Value --> Real

2.3 古典派とロンドン派における統合テスト

このセクションの結論は、**「ロンドン派の定義では、統合テストの範囲が広くなりすぎる(実用的な単体テストすら統合テスト扱いされてしまう)」**という点にあります。

定義の食い違い(比較表)

学派隔離の対象単体テストの定義統合テストの定義実務への影響
ロンドン派
(モック主義)
SUT (テスト対象クラス)
SUT以外の依存は全てモック化する。
クラス単体で完結し、他クラスを一切使わないテスト。実物の依存オブジェクトを1つでも使うテスト。
(例: ドメインオブジェクト同士の連携テスト)
ビジネスロジックの連携テストまで「統合テスト」扱いになるため、単体テストが極端に細かくなりすぎる。
古典派
(デトロイト派)
Unit Test (テストケース)
テスト同士がお互いに影響しなければOK。
共有依存(DB等)以外の実物クラスを自由に使うテスト。共有依存(プロセス外依存)にアクセスするテスト。ドメインロジックの連携は「単体テスト」として扱える。実用的。

なぜこれが問題なのか?

以下の図を見てください。複雑なビジネスロジックがあり、Order(注文)クラスが PriceCalculator(価格計算)クラスを使っているとします。

flowchart LR
    Test[テストコード] --> Order[Order クラス]
    Order --> Calculator[PriceCalculator クラス]
    Order --> DB[(データベース)]

    style DB fill:#f9f,stroke:#333,stroke-width:2px
  • 古典派の視点:
    • DB は共有依存なので、これに繋ぐなら「統合テスト」。
    • Calculator はただのロジック(オンメモリ)なので、これを使うテストは**「単体テスト」**。
  • ロンドン派の視点:
    • Calculator も外部依存(Collaborator)とみなす。
    • したがって、Calculator の実物を使って Order をテストしたら、それは**「統合テスト」**になってしまう。

ロンドン派の定義に従うと、単純なオブジェクトの組み合わせテストすら「統合テスト」に分類されてしまいます。これは統合テストという言葉の有用性を下げてしまいます。

エンドツーエンド(E2E)テストの位置づけ

この文脈において、E2Eテストは「統合テストのサブセット(部分集合)」として定義されます。

  • 統合テスト: 1つ以上の共有依存(DBなど)を含むテスト。
  • E2Eテスト: すべての 共有依存を含むテスト。

エンジニアが採用すべき視点(著者の結論)

著者はここでも古典派の定義を支持しています。

単体テストか統合テストかの境界線は、**「プロセス外依存(共有依存)があるかどうか」**という1点に置くべきです。

そうすることで、開発者は以下のようにシンプルに行動できます。

  1. ドメインロジック(複雑な計算やルール):
    • どんなにクラスが連携しようと、DBに触れないなら**「単体テスト」**として高速に実行する。
  2. 永続化や外部通信:
    • DBやAPIに触れる部分のみを**「統合テスト」**として隔離し、数を絞って管理する。

第3章:単体テストの構造(The Anatomy of a Unit Test)

実用的なテストコードを書くための具体的な構造について解説しています。

3.1 AAAパターン(Arrange-Act-Assert)

すべての単体テストは、以下の3つのフェーズで構成されるべきです。これは「Given-When-Then」とも対応します。

  1. 準備(Arrange): テスト対象システム(SUT)とその依存関係をセットアップし、期待する状態にする。
  2. 実行(Act): テスト対象のメソッドや振る舞いを実行する。
  3. 確認(Assert): 結果が期待通りか検証する。

エンジニア向けTip:

  • 各セクションを視覚的に分けるために、空行を入れるか、コメント(// Arrangeなど)を入れること。
  • Actセクションは極力1行にする。もし複数行必要なら、API設計に問題がある可能性がある。

3.2 テスト内での「if文」や「ループ」は禁止

テストコードの中に条件分岐(if)やループ(for)がある場合、それはテスト自体が複雑になりすぎている証拠です。テストはサイクロマティック複雑度を0に保ち、読み手が「一目で何をしているか」わかるようにしなければなりません。

3.3 テストフィクスチャの再利用

テスト間のコード重複を防ぐために、セットアップロジックを共通化しますが、やり方には注意が必要です。

手法推奨度理由
コンストラクタでの初期化❌ 非推奨テスト間の結合度が高まる。あるテストのための修正が、別のテストに影響する。可読性が下がる。
プライベートファクトリメソッド✅ 推奨テストコード内で明示的に呼び出すため、何がセットアップされたか明確。
Object Mother / Builder✅ 推奨複雑なオブジェクト生成が必要な場合、専用の生成クラスを作る(TestDataBuilderパターン)。

3.4 テストの命名規則

「メソッド名_条件_結果」のような命名(例: Sum_TwoNumbers_ReturnSum)は、実装詳細に依存するため推奨されません。

**「ドメインに精通した非エンジニア(ビジネス担当者)が読んでも意味がわかる名前」**を目指すべきです。

  • 悪い例: IsDeliveryValid_InvalidDate_ReturnsFalse
  • 良い例: Delivery_with_a_past_date_is_invalid (過去の日付での配送は無効である)

テスト名は、コードの「事実」ではなく、振る舞いの「結果」を物語るべきである。

単体テストとその価値

第4章:良い単体テストの4本の柱(The Four Pillars of a Good Unit Test)

どのようなテストが良いテストなのか。著者は以下の4つの柱(指標)ですべてのテストを評価できると断言しています。

4.1 4本の柱の定義

テストコードの価値は、以下の4つの属性の掛け合わせで決まります。どれか一つでもゼロであれば、そのテストの価値はゼロです。

説明エンジニアへの意味合い
1. リグレッションの防止
(Protection against regressions)
バグ(機能退行)をどれだけ防げるか。実行されるコード量が多いほど、またコードが複雑であるほど、スコアが高くなる。
2. リファクタリングへの耐性
(Resistance to refactoring)
コードを書き換えたときに、機能が壊れていないのにテストが失敗しないか。最も軽視されがちだが、長期的なメンテナンスにおいて最も重要な柱。
3. 迅速なフィードバック
(Fast feedback)
テストがどれだけ速く実行できるか。遅いテストは実行されなくなる。開発のサイクルを回すために必須。
4. 保守性
(Maintainability)
テスト自体の読みやすさと、セットアップの難易度。難しいセットアップ(巨大なDBシード等)や、理解不能なテストコードは保守性を下げる。

4.2 第1の柱:リグレッションの防止

単に「コードを実行した」だけでは不十分です。

  • コードの複雑さ: 単純なgetter/setterではなく、複雑なビジネスロジックをテストしているか。
  • ドメインの重要性: ビジネスの中核となる機能をテストしているか。

4.3 第2の柱:リファクタリングへの耐性(最重要)

ここが本章のハイライトです。

機能を変えずにコードの構造を改善することを「リファクタリング」と呼びますが、このときに「機能は正しいのにテストが落ちる」現象を「偽陽性(False Positive)」と呼びます。

偽陽性の何が悪いのか?

  1. 狼少年になる:開発者がテストのエラーメッセージを無視するようになる。
  2. リファクタリングの意欲を削ぐ:「触るとテストが壊れるから」と、汚いコードが放置される。

偽陽性を防ぐ唯一の方法は、「実装の詳細(How)」ではなく「最終的な結果(What)」をテストすることです。

4.4 最初の2つの柱の関係

「リグレッションの防止」と「リファクタリングへの耐性」は、テストの正確性を表す表裏の関係です。

  • 偽陰性(False Negative): バグがあるのにテストが通ってしまうこと。 → リグレッション防止で防ぐ。
  • 偽陽性(False Positive): バグがないのにテストが落ちること。 → リファクタリング耐性で防ぐ。

4.5 第3・第4の柱とトレードオフ

4つの柱すべてを最大化することは不可能です。これはCAP定理に似ています。

  • E2Eテスト: リグレッション防止・リファクタリング耐性は最高だが、フィードバックが遅い
  • 詳細な単体テスト: フィードバックは早いが、実装詳細に結合しすぎてリファクタリング耐性が低い(ロンドン派のテストなど)。

理想的なテストは、フィードバック速度を犠牲にしない範囲で、リグレッション防止とリファクタリング耐性を最大化したものです。著者は、「リファクタリングへの耐性」だけは妥協してはならない(0か1かしかない)と説きます。


第5章:モックとテストの脆弱性(Mocks and Test Fragility)

第4章で定義した「リファクタリングへの耐性」を高めるためには、テストダブル(モックやスタブ)の正しい理解が不可欠です。本章では、なぜモックがテストを脆弱にするのかを解明します。

5.1 モックとスタブの違い

世の中には多くのテストダブル(Dummy, Spy, Mock, Stub, Fake)がありますが、著者は大きく2つに分類します。

分類名称役割注目ポイント具体例
モック
(Mock系)
Mocks, Spies外部への出力を検証する。相互作用(Interaction)メール送信サービスへの呼び出し確認、メッセージキューへの発行。
スタブ
(Stub系)
Stubs, Dummies, Fakes内部への入力を代用する。状態(State)DBからのデータ取得の偽装、設定ファイルの読み込み偽装。

重要なルール:

モック(出力)はアサート(検証)してもよいが、スタブ(入力)をアサートしてはいけない。

スタブは「テストの事前条件」に過ぎないため、スタブがどう呼ばれたかを検証するのは実装詳細への結合(=リファクタリング耐性の低下)につながります。

5.2 観察可能な振る舞い vs 実装の詳細

リファクタリング耐性のあるテストを書くには、「観察可能な振る舞い(Observable Behavior)」のみをテストし、それ以外をすべて無視する必要があります。

観察可能な振る舞いとは?

以下の2つの条件を両方満たすものです。

  1. クライアント(呼び出し元)にとっての目標であること。
  2. 外部アプリケーションから見えること(パブリックAPI)。

逆に、これに当てはまらないものはすべて「実装の詳細」であり、テストしてはいけません。

graph TD
    Code[コード]
    Code --> Public[パブリックAPI]
    Code --> Private[プライベートAPI]
    
    Public --> Behavior{観察可能な振る舞いか?}
    Private --> Detail[実装詳細<br>テストするな!]
    
    Behavior -- Yes --> Test[テスト対象]
    Behavior -- No --> Detail

よくある間違い:

「パブリックメソッドだからテストする」は間違いです。パブリックであっても、単なるヘルパーメソッドや、クライアントの目標に直結しないものは実装詳細です。

5.3 六角形アーキテクチャ(Hexagonal Architecture)との関係

著者は、良いテスト設計は良いアプリケーション設計と表裏一体であるとし、六角形アーキテクチャ(ヘキサゴナル)を推奨しています。

  • ドメイン層: ビジネスロジックの中心。外部依存を持たない。 → 単体テストの主戦場。
  • アプリケーションサービス層: ドメイン層と外部(DBなど)を取り持つ。 → ここが「テストの単位」となるべき。

テストは、アプリケーションサービス層(コントローラーやユースケース)に対して行い、ドメインモデルのメソッドを直接個別にテストするのは避けるべき場合がある(ドメインが複雑な場合を除く)。

5.4 モックの正しい使い方(システム内 vs システム外)

ここが非常に実践的なアドバイスです。

  • システム内通信(Intra-system): アプリケーション内部のクラス間のやり取り。
    • モック使用禁止。実物を使うこと。これをモックにするとテストが壊れやすくなる。
  • システム間通信(Inter-system): アプリケーションと外部システム(メールサーバー、決済APIなど)のやり取り。
    • モック使用推奨。これらは制御できない「共有依存」であり、かつ副作用(メールが飛ぶ等)があるため、正しく呼び出されたか(相互作用)を検証する必要がある。

第6章:単体テストのスタイル(Styles of Unit Testing)

単体テストには主に3つのスタイルが存在します。著者はそれぞれのスタイルを「4本の柱」で評価し、最強のスタイルである「出力ベース」への移行を推奨しています。

6.1 単体テストの3つのスタイル

スタイル特徴
1. 出力ベース
(Output-based)
入力を与え、戻り値(出力)だけを検証する。副作用がない純粋関数にのみ適用可能。int result = Math.Add(2, 3);
Assert.Equal(5, result);
2. 状態ベース
(State-based)
メソッド実行後のシステムの状態(変数の値など)を検証する。古典派でよく使われる。user.AddProduct(product);
Assert.Equal(1, user.Products.Count);
3. 通信ベース
(Communication-based)
テスト対象が協力者(Collaborator)を正しく呼び出したかを検証する。モックを使用する。mockEmailer.Verify(x => x.Send(), Times.Once);

6.2 スタイルの比較評価

第4章の「4本の柱」を使って採点すると、以下のようになります。

指標出力ベース状態ベース通信ベース
リファクタリング耐性最高普通低い
保守性最高普通低い
リグレッション防止良い良い良い
フィードバック速度速い速い速い

結論: 出力ベース(関数型)のテストが最も品質が高い。

状態管理やモックのセットアップが不要で、テストコードが圧倒的にシンプルになるためです。

6.3 関数型アーキテクチャ(Functional Architecture)

出力ベースのテストを行うためには、プロダクトコードを「副作用のないコード」にする必要があります。しかし、実際のアプリにはDB保存などの副作用が不可欠です。

そこで著者が提案するのが、**「関数型コア(Functional Core)」と「可変シェル(Mutable Shell)」**の分離です。

  • 関数型コア(Functional Core):
    • ビジネスロジックの集合体。
    • データを変更せず、新しいデータを返す(不変性)。
    • ここを徹底的に出力ベースで単体テストする。
  • 可変シェル(Mutable Shell):
    • 入力(DBやユーザー)を受け取り、コアに渡し、結果をDBに保存したり画面に出力したりする。
    • ロジックは持たず、配線だけを行う。
    • ここは統合テストでカバーする。
flowchart TB
    subgraph Shell ["可変シェル (Mutable Shell)"]
        direction TB
        Input["入力の収集<br>(DB, APIなど)"]
        Output["副作用の適用<br>(DB保存, レスポンス)"]
    end

    subgraph Core ["関数型コア (Functional Core)"]
        Logic["純粋なビジネスロジック<br>(決定論的・副作用なし)<br>【ここをテストする】"]
    end

    Input --> Logic
    Logic --> Output

    style Core fill:#e6fffa,stroke:#009688,stroke-width:2px
    style Shell fill:#fff9c4,stroke:#fbc02d,stroke-width:2px

第7章:価値のある単体テストへのリファクタリング(Refactoring toward Valuable Unit Tests)

すべてのコードを平等にテストする必要はありません。第7章では、コードを4つのタイプに分類し、それぞれに対する戦略とリファクタリング手法(ハンブルオブジェクトパターン)を解説します。

7.1 コードの4分類(The 4 Types of Code)

コードの「複雑さ/ドメインの重要性」と「協力者(依存関係)の多さ」を軸に分類します。

flowchart TD
    %% ノードの定義
    Start["コードの分析"]
    CheckComplex{"複雑さ・重要性は高いか?"}
    CheckCollab1{"協力者(依存)は多いか?"}
    CheckCollab2{"協力者(依存)は多いか?"}
    
    OverComplicated["過度に複雑なコード<br>【リファクタリング必須】<br>(ハンブルオブジェクト化)"]
    Domain["ドメインモデル<br>【徹底的に単体テスト】<br>(最も価値が高い)"]
    Controller["コントローラー<br>【統合テスト】"]
    Trivial["自明なコード<br>【テスト不要】"]

    %% 接続の定義
    Start --> CheckComplex
    CheckComplex -- YES --> CheckCollab1
    CheckComplex -- NO --> CheckCollab2
    
    CheckCollab1 -- YES --> OverComplicated
    CheckCollab1 -- NO --> Domain
    
    CheckCollab2 -- YES --> Controller
    CheckCollab2 -- NO --> Trivial

各象限の戦略

分類戦略理由
1. ドメインモデル & アルゴリズム
(左上: 高複雑・低依存)
徹底的に単体テストここがアプリの心臓部であり、依存が少ないためテストコストも低い。最も費用対効果が高い。
2. 自明なコード
(左下: 低複雑・低依存)
テストしないGetter/Setterや単純な変数代入。テストしてもバグ発見率が低く、時間の無駄。
3. コントローラー
(右下: 低複雑・高依存)
統合テスト外部との連携のみを担当。ロジックがないなら単体テストは不要。接続確認を行う。
4. 過度に複雑なコード
(右上: 高複雑・高依存)
リファクタリング対象最も危険。 ロジックが複雑なのに、DBなどの依存も持っている(Fat Controllerなど)。テストを書くのが非常に難しい。

7.2 ハンブルオブジェクト(Humble Object)パターン

「過度に複雑なコード(右上)」をテスト可能にするための必殺技がハンブルオブジェクトパターンです。

複雑でテストしにくいコードから「ロジック」を抽出して別のクラス(ドメインモデル)に移動させます。残ったクラスは、ただ依存関係を呼び出すだけの「謙虚(Humble)で薄っぺらい」存在にします。

目的: 「過度に複雑なコード(右上)」を、「ドメインモデル(左上)」と「コントローラー(右下)」に分割すること。

適用例:非同期処理やUIを持つクラス

  • 適用前: UserManager クラス内で、「ユーザーの年齢を計算する複雑なロジック」と「DBへの保存」が混在している。 → テスト困難。
  • 適用後:
    1. User クラス(ロジック担当): 年齢計算ロジックのみを持つ。依存なし。 → 単体テスト可能。
    2. UserManager クラス(ハンブル): User で計算させて、DBに保存するだけ。 → 統合テストのみ。

7.3 価値のあるテストへのリファクタリング手順

著者は具体的なリファクタリングのステップを提示しています。

  1. 暗黙的な依存を明示的にする: 内部で new Date()Database.Instance を使わず、引数として受け取る。
  2. 副作用を分離する: メソッド内で「計算」と「状態変更」を同時に行わない(コマンド・クエリ分離の原則)。
  3. ドメイン層へのロジック移動: サービス層にある if 文や計算ロジックを、エンティティや値オブジェクトに移動させる。

エンジニアへの金言:

「テストを書くのが難しい」と感じたら、それはテストの書き方が悪いのではなく、コードの設計が悪い(結合度が高すぎる)というシグナルである。

第8章:統合テストを行う理由(Why Integration Testing?)

第2章で、統合テストは「プロセス外依存(共有依存)を経由するテスト」と定義されました。本章では、統合テストをどのように設計すれば最大の効果が得られるかを解説します。

8.1 統合テストの役割

単体テスト(ドメインロジックの検証)だけでは不十分です。

統合テストの役割は、「システム全体が連携して正しく機能するか」を確認することであり、具体的には以下の2点に絞られます。

  1. ハッピーパス(正常系)の確認: 最も長い、主要なビジネスフローが通るか。
  2. エッジケース(異常系)の確認: 特に「外部システムがエラーを返した時」の挙動。

8.2 プロセス外依存の2分類(重要)

すべての外部システムを同じように扱う必要はありません。著者はこれを2つに分類しています。

分類定義扱い方具体例
管理下の依存
(Managed Dependency)
アプリケーションから完全に制御できるプロセス外依存。外部からアクセスされない。実物を使う
(統合テスト)
アプリ専用のデータベース (SQL Server, MySQLなど)
管理外の依存
(Unmanaged Dependency)
他のアプリケーションと共有されている、または制御できない依存。モック化する
(単体テスト)
SMTPサーバー、決済ゲートウェイ、メッセージバス

エンジニアへの指針:

  • DB は「管理下の依存」なので、モックにせず、Dockerなどで実物を立ち上げてテストする。
  • メール送信 は「管理外の依存」なので、副作用を防ぐためにモックを使う。

8.3 統合テストでインターフェースを使うな

「インターフェースはテストのためにある」という誤解がありますが、ドメインロジックとデータベースの間にインターフェースを挟む必要はありません。

  • 管理下の依存(DB)に対してインターフェースとモックを使うと、統合テストの価値(SQLの検証など)が失われます。
  • 実物のリポジトリクラスをそのまま使い、実物のDBに対してテストを実行してください。

第9章:モックのベストプラクティス(Mocking Best Practices)

モックは諸刃の剣です。第5章の議論を深め、より実践的なテクニックを紹介します。

9.1 システムの末端でのみモックする

モックは、システムと外部世界(管理外の依存)との**境界線(Boundary)**でのみ使用すべきです。

  • 悪い例: OrderServiceCustomerService(ドメインロジック)をモックする。
  • 良い例: OrderServiceIEmailGateway(外部接続のアダプター)をモックする。

9.2 スパイ(Spy)の使用推奨

検証には、一般的なモックフレームワークの機能よりも、手書きの**スパイ(Spy)**の使用を推奨しています。 検証ロジックを自分で書くことで、テストが読みやすくなり、ライブラリへの依存も減ります。

9.3 統合テストにおけるモック

統合テストでは、管理外の依存(メールサービスなど)のみをモックに置き換えます。 このとき、モックが正しく呼び出されたかどうかだけでなく、「モックに渡されたデータが正しいか」もしっかり検証してください。

第10章 データベースのテスト

統合テストにおいて、データベース(以下DB)は単なる外部システムではなく、アプリケーションの一部(管理下の依存)として扱わなければなりません。

10.1 前提:データベースは「モック」してはいけない

多くのエンジニアが「テストを速くしたい」という理由で、DBをモック化したり、インメモリDBに置き換えたりします。しかし、著者はこれを強く否定します。

なぜ「インメモリDB(SQLite Memory / EF Core InMemory)」はダメなのか?

インメモリDBは本番のDB(PostgreSQL, MySQL, SQL Server等)とは「別物」だからです。

問題点説明具体例
偽陰性 (False Negative)バグがあるのにテストが通ってしまう。本番DBなら外部キー制約違反になるデータでも、インメモリDBだと保存できてしまう。
偽陽性 (False Positive)バグがないのにテストが落ちる。DB固有のSQL関数(日付操作やJSON処理など)がインメモリDBでサポートされておらずエラーになる。
機能の欠如そもそもテストできない機能がある。トランザクション分離レベル、ストアドプロシージャ、特定の型(Geometry型など)。

鉄則:

統合テストには、本番環境と同じ種類のDBMS(Database Management System)を使用せよ。現在はDockerコンテナがあるため、これを実現するコストは非常に低い。

10.2 テストデータの管理:クリーンアップ戦略

DBテストで最も難しいのは、「前のテストデータが残っていて次のテストが落ちる(Flaky Test)」問題です。これを防ぐための3つの戦略を比較します。

戦略比較表

戦略仕組みメリットデメリット著者の判定
1. トランザクション・ロールバックテスト開始時に BeginTransaction、終了時に Rollback高速。並列実行しやすい。コミット時のエラー(制約違反など)を検知できない場合がある。「トランザクション内トランザクション」など複雑な制御が必要。⚠️
2. テスト終了時に削除
(Teardown Cleanup)
テストの TearDown でデータを消す。直感的。テストプロセスが強制終了(クラッシュ)するとデータが残り、後のテストが全滅する。
3. テスト開始時に削除
(Setup Cleanup)
テストの SetUp で全データを消す(TRUNCATE等)。最も確実。前のテストがどう終わろうと、クリーンな状態で始められる。データ量が多いと少し遅くなる(が、現代のDBなら十分速い)。推奨

推奨フロー:開始時クリーンアップ

flowchart TD
    Start((テスト開始)) --> Clean["既存データの消去<br>(TRUNCATE / DELETE ALL)"]
    Clean --> Arrange["データのセットアップ<br>(Object Mother等でINSERT)"]
    Arrange --> Act["SUTの実行"]
    Act --> Assert["DBの状態検証"]
    Assert --> End((終了))
    
    subgraph KeyPoint ["重要ポイント"]
        Clean -- "前のテストのゴミを一掃" --> Arrange
        Assert -- "データは消さずに残す" --> End
    end

Tip: テスト失敗時にデータがDBに残っていると、開発者がDBの中身を見てデバッグできるというメリットもあります。

10.3 テストデータの準備(Arrangeフェーズ)

テストごとにSQLの INSERT 文を何十行も書くのは保守性の悪夢です。

著者はObject Motherパターン(またはTest Data Builderパターン)の導入を推奨しています。

  • 悪い例: テストメソッド内で new User(...), new Order(...) して repo.Save() を繰り返す。必須フィールドが増えるたびに全テストの修正が必要になる。
  • 良い例: CreateUser() のようなファクトリメソッドを用意し、デフォルト値で埋めてくれるようにする。必要な箇所だけ上書きする。
// テストコードの可読性が劇的に上がる
var user = aUser().WithEmail("test@example.com").Build();
saveToDb(user);

10.4 読み取り(Reads)と書き込み(Writes)のテスト戦略

CQRS(コマンド・クエリ責務分離)の考え方はテストにも適用されます。

書き込み(Writes / Command)のテスト

最も重要です。 多くのビジネスロジックは「データをどう変更するか」に含まれるからです。

  • 検証方法: SUT(System Under Test)を実行した後、DBから直接データを再取得して検証します。
  • 注意: SUTが使ったリポジトリインスタンスのキャッシュ(EF CoreのChangeTrackerなど)を検証してはいけません。必ず新しいコンテキスト(接続)でDBを見に行き、本当に永続化されたかを確認します。

読み取り(Reads / Query)のテスト

書き込みに比べると重要度は下がります。

  • 単純な FindByIdGetAll などのテストは、コストに見合いません(ライブラリが動くのは当たり前だから)。
  • テストすべきケース:
    • 複雑なSQLを含むレポート出力。
    • 多数の JOINGROUP BY を含む検索処理。
    • これらは、バグを発見するというより、「SQLの構文エラーがないか」「マッピングが正しいか」を確認する意味合いが強いです。

10.5 リポジトリを単体テストすべきか?

よくある間違いとして、「リポジトリクラスの全メソッドに対して単体テストを書く」というものがあります。

著者はこれを「時間の無駄」と切り捨てています。

  • リポジトリは「アプリケーションサービス」から使われる部品です。
  • アプリケーションサービスの統合テストを行えば、リポジトリのコードも(DBも含めて)自動的に実行・検証されます
  • リポジトリ単体のテストが必要なのは、そのリポジトリが非常に複雑な動的クエリを生成する場合などに限られます。

第11章:単体テストのアンチパターン(Unit Testing Anti-patterns)

最終章では、現場でよく見かける「やってはいけないこと」をリストアップしています。

11.1 プライベートメソッドの単体テスト

絶対にしてはいけません。 プライベートメソッドは「実装の詳細」です。これをテストするとリファクタリング耐性がゼロになります。パブリックAPI経由で間接的にテストしてください。 もしプライベートメソッドが複雑すぎてテストしたい場合は、そのメソッドを別のクラス(ドメインクラス)に抽出してパブリックにし、そこでテストしてください。

11.2 状態の公開(Exposing Private State)

テストのために private フィールドを publicinternal にしてはいけません。これも実装詳細への結合です。

11.3 ドメインロジックの漏洩(Domain Logic Leakage)

テストコードの中に、複雑なロジックを書いてはいけません。

// 悪い例: テストコード内で期待値を計算している
var expected = 100 * 1.08; 
Assert.Equal(expected, actual);

これは「アルゴリズムの重複」であり、プロダクトコードと同じバグをテストコードにも埋め込むリスクがあります。期待値はハードコード(108)するか、信頼できる別の手段で算出してください。

11.4 時間(現在時刻)のテスト

DateTime.Now をコード内で直接使うと、テストのたびに結果が変わり(非決定論的)、テストが失敗します。 対策: 時刻を引数として渡すか、TimeProvider のような抽象化を行い、テスト時は時刻を固定(Stub)できるようにします。

書籍全体のまとめとアクションプラン

『単体テストの考え方/使い方』は、単なるTDDの教本ではなく、「ソフトウェアの保守性と成長」を主眼に置いたアーキテクチャの本でした。

最終アクションプラン:エンジニアとしての次のステップ

明日からあなたの現場を変えるためのロードマップです。

  1. 「ピラミッド」ではなく「ダイヤモンド」を目指すかも考える
    • もしドメインロジックが薄く、DB操作が中心のアプリなら、無理に単体テストを書かず、「管理下のDBを使った統合テスト」を主体にする(単体テストが少なく統合テストが多い構成)のも、この本的には「アリ」です。プロジェクトの性質に合わせてください。
  2. Docker環境の整備
    • ローカル開発環境で docker-compose up 一発でDBが立ち上がり、統合テストが走る環境を作ってください。インメモリDBへの逃げ道を断ちましょう。
  3. テストコードの「リファクタリング耐性」チェック
    • チームのコードレビューで、テストコードに対してこう問いかけてください。
    • 「このメソッドの中身(実装)を書き換えたら、このテストは落ちますか?」
    • もし「機能は変わらないのに落ちる」なら、そのテストは修正が必要です。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次