はじめに
「単体テストの考え方/使い方」を読んでみたので感想を書きます。
対象者
この記事は下記のような人を対象にしています。
- 中級エンジニア
- テスト設計に悩んでいる
- 単体テストと統合テストの違いがよくわからない人
結論
- 単体テストと統合テストを組み合わせることで、継続可能な開発を実現できる!
- そのためにはテストが描きやすいプロダクションコードにする必要があるので勉強しよう!
第1章: なぜ、単体テストを行うのか?
- コード網羅率はいくらでもハックできる
- 分岐網羅率の方がまだマシ、でもハックできる
- 網羅率が低い場合はテスト品質が低い、と言える
- が、網羅率が高いからといってテスト品質が高いとは言えない
- ドメインモデルに対してテストが書かれていることが重要
- インフラ層に対しては単体テストは不要
第2章: 単体テストとは何か?
- 古典派(デトロイト派)
- テストケース同士が隔離されていること
- テストケースごとにオブジェクトをインスタンス化していればOK
- テストが壊れにくい(=メンテナンスが楽)
- テストケース同士が隔離されていること
- ロンドン派(モック派)
- クラス同士が隔離されていること
- テスト粒度が細かくなる
- 複雑な依存関係を簡単にテストできる
- テストが失敗した時の原因究明がしやすい
- TDD(テスト駆動開発)に向いている
- テストが壊れやすい(=メンテナンスが大変)
第3章: 単体テストの構造的解析
- AAAパターン
- Arrange
- Act
- Assert
- Given-When-Thenも似た構造
- テストケース内でifは使うな(テストケースを分けよう)
- Arrangeフェーズが大きすぎる場合、元のクラスの分割を検討しよう
- Actフェーズが1行以上になる場合はクラス設計を見直そう
- AAAの間に改行を入れるとテストが読みやすくなる
- テストフィクスチャはコンストラクタではなく、プライベートメソッド化しよう
- テスト名は「〇〇な条件の時に××になる」のような形式が好ましい
- 複数の条件でテストしたいときはパラメータ化すべき
第4章: 良い単体テストを構成する4本の柱
- 1.バグに対する保護
- テストの際にできるだけ多くのプロダクションコードを実行する
- 偽陰性(本来の振る舞いはNGなのにテストが通ってしまう)を防ぐ
- 「テストをすることでバグを見つける」
- プロジェクトの初期に重視される
- 2.リファクタリングへの耐性
- リファクタリングしてもテストが失敗しないこと
- 偽陽性(本来の振る舞いはOKなのに、テストが失敗する)を防ぐ
- 「テストをすることでバグがないことを示す」
- プロジェクトの中期〜後期に重要視される
- テストコードがプロダクションコードと深く結びつくほど、リファクタへの耐性が下がる
- 実装の詳細をテストするのではなく、観察可能な振る舞いをテストすべき
- 3.迅速なフィードバック
- 4.保守のしやすさ
- 4つ全ての要素をMAXにすることはできない
- 1,2,3の要素は排反するから
- E2Eテストは1,2を満たすが、3は満たさない
- 単純なテストを書くことは2,3を満たすが、1は満たさない
- 壊れやすいテストは1,3を満たすが、2は満たさない
- ベストな戦略は「リファクタへの耐性」「保守のしやしさ」をMAXにしつつ、「バグに対する保護」「迅速なフィードバック」のバランスをとること
- つまり、E2Eテスト、統合テスト、単体テストを併用する
- テストケースを作るときはブラックボックスで
- テストケースを分析するときはホワイトボックスで
第5章: モックの利用とテストの壊れやすさ
- テストダブルは2種に分類される
- 出力(例:メール送信)を模倣し、検証する
- モック:フレームワークにより作成
- スパイ:開発者が作成(手書きのモック)
- 入力(例:データ取得)を模倣する
- ダミー:ハードコーディングされた値
- スタブ:設定によって出力が変更できる
- フェイク:まだ存在しない依存を置き換える
- 出力(例:メール送信)を模倣し、検証する
- カプセル化
- 公開されたAPIは観察可能な振る舞いを担当
- 何らかの操作(計算したり、副作用を生むこと)
- システムの状態を示すこと
- プライベートなAPIは実装の詳細を担当
- 観察可能な振る舞いに該当しない全てのコード
- APIを適切に設計すると、単体テストが描きやすくなる
- 公開されたAPIは観察可能な振る舞いを担当
第6章: 単体テストの3つの手法
- 出力値ベーステスト
- 状態ベーステスト
- コミュニケーションベーステスト
| 出力値テスト | 状態テスト | コミュニケーションテスト | |
|---|---|---|---|
| バグからの保護 | 影響なし | 影響なし | 影響なし |
| リファクタ耐性 | 高い | 低い | より低い |
| 迅速なフィードバック | 影響なし | 影響なし | 影響なし |
| 保守しやすさ | しやすい | やや難しい | 難しい |
- 関数型アーキテクチャ(決定を下すコードとアクションを実行するコードを分離する)を用いると出力値テストを導入しやすくなる
- ヘキサゴナルアーキテクチャでドメイン層とアプリケーション層を分けるのと同じ
第7章: 単体テストの価値を高めるリファクタリング
- 質素なオブジェクト(=コントローラのこと)を用いてドメイン層とアプリケーション層を分離する
| No | コントローラの簡潔さ | ドメインモデルのテストしやすさ | パフォーマンスの高さ |
|---|---|---|---|
| 外部依存をドメインに注入 | ○ | × | ○ |
| 読み込みを先頭に、書き込みを最後にする | ○ | ○ | × |
| 決定を下す過程を細分化 | × | ○ | ○ |
- 「確認後実行パターン」「ドメインイベント」の導入により、コントローラから決定を下すコードを隔離できる
第8章: なぜ、統合(integration)テストを行うのか?
- 早期失敗(Fail Fast)を導入すると、統合テストの代わりになる
- プロセス外依存は2種類
- 管理下にある依存
- テスト対象のアプリしかアクセスしないDBなど
- 実装の詳細になる
- 統合テストではモックを使用しない
- 管理下にない依存
- メールサービスなど
- 観察可能な振る舞いの一部
- 統合テストではモックを使用する
- 管理下にある依存
- インターフェースを導入する理由
- プロセスの依存を抽象化し、疎結合を実現する
- もし実装が1種しかない場合は不要。
- 実装が2種以上になって初めて導入すべき。
- テストの際にモックを利用できるようにする
- こちらが大きなメリット。
- つまり、管理下にない依存だけインターフェースを導入すべき
- プロセスの依存を抽象化し、疎結合を実現する
- 統合テストのベストプラクティスは3種
- ドメインモデルの境界の明確化
- アプリケーション層の圧縮
- 循環依存の排除
第9章: モックのベスト・プラクティス
- モックの利用は統合テストに限定する
- ドメインモデルの検証は単体テスト
- コントローラの検証は統合テスト
- モックに対して行われた呼び出しの回数を確認する
- モックの対象になる型は自身のプロジェクトが所有する型に限定する
第10章: データベースに対するテスト
- 開発者ごとに個別のDBインスタンスを持つのが理想
- DBの変更方法は2種
- 状態ベース
- 移行ベース
- トランザクション
- Unit of Work
第11章: 単体テストのアンチ・パターン
- privateメソッドはテストすべきではない
- privateな状態を公開すべきではない
- ドメイン知識がテストに記述されてはいけない
- テスト用の記述がプロダクションコードに記載されてはいけない
おわりに
「単体テストの考え方/使い方」についてまとめました。