Open5
単体テストの考え方/使い方 まとめ
概要
- 自分の学習用に、「Vladimir Khorikov - 単体テストの考え方/使い方」 の内容を各章ごとにまとめる。
構成
- 1部 単体テストとは
- 単体テストの概要について
- 2部 単体テストとその価値
- 単体テストの良し悪しを評価する方法について
- 単体テストの価値を高める方法について
- 3部 統合テスト
- 4部 単体テストのアンチパターン
[1部 単体テストとは] 1章 なぜ、単体テストを行うのか
- 現状
- 単体テストは書いて当たり前
- 「良い単体テストとは?」が求められる
- 単体テストの目的
- ソフトウェアを「持続可能」にするため
- ソフトウェア・エントロピーの上昇に対し、regressionを防ぐ手段
- テストケースの質で、成長にかかる労力の低減具合が変わる
- 質の良いテストケースだけをテストスイートに含めること
- ソフトウェアを「持続可能」にするため
カバレッジ
- カバレッジは、テストスイートの質の悪さは示せても、質の良さを必ずしも示さない!
- コード網羅率
- 行数ベース。総行数に対する実行された行数の割合
- 関係ない要素で算出結果が変わる
- 分岐網羅率
- 分岐パスベース。分岐経路の総数に対する経由された経路の数の割合
- カバレッジを盲信した場合の問題点
- 網羅率からは実際にテスト対象のコードが検証されたのかを保証できない
- assertしなくても網羅率は上がるから
- 網羅率の算出時、ライブラリ内のコードは計測の対象から外れる
- 網羅率からは実際にテスト対象のコードが検証されたのかを保証できない
質の良いテストスイート
- 前提
- テストスイート全体の評価を一度に行える指標などはない。個別に判断するしかない。
- 特徴
- テスト実行が開発サイクルに組み込まれている
- コードベースの特に重要な部分のみがテスト対象となっている
- 最も効果的なものは、ドメインモデルに対するテスト
- それ以外
- インフラ関連コード、外部サービスや依存関係、構成要素同士を結びつけるコード
- ドメインモデルがよく分離されている必要がある
- 最小限の保守コストで最大限の価値を生み出すようになっている
- そのために必要なこと
- 価値あるテストケースを認識できる
- 価値あるテストケースを作成できる
- 評価基準を知った上で、しっかりとした設計ができる必要がある
- そのために必要なこと
[1部 単体テストとは] 2章 単体テストとは何か?
- 単体テスト
- 少数のコード (単体) の検証
- 実行時間が短い
- 隔離された状態で行う
→ 「単体」と「隔離」に関する2つの解釈 → ロンドン学派 / 古典学派
ロンドン学派と古典学派
ロンドン学派
- 解釈
- テスト対象を依存から隔離する
- 単体は単一のクラスやメソッド
- 特徴
- 依存をテストダブルに置換する
- リテラルや値オブジェクトを除くすべての依存が対象
- Assertは「やりとり」に対して行う
- 〇〇メソッドを〇〇回呼んだ、等
- 依存をテストダブルに置換する
- 利点
- 問題の特定が容易
- 複雑な依存関係から隔離できる
- テストとテスト対象が1:1で紐づく
古典学派
- 解釈
- テストケースを他のテストケースから隔離する
- 単体は複数のクラスやメソッドを含みうる
- 特徴
- テストダブル化するのは共有依存のみ
- 共有依存 = テストケース間で共有された状態。グローバル変数や、DB, ファイルシステム等のプロセス外依存など
- 共有依存のテストダブル化により、状態の共有が断ち切られ、テストの実行速度向上につながる
- テストダブル化するのは共有依存のみ
- 利点
- テスト実行を並列化できる
比較
ロンドン学派のメリットは以下の通り
- より細やかな粒度で検証ができる
- が、対象コードの粒度はテストのよさとあまり関係がない。1単位のコードの検証であることよりも、1単位の振る舞いの検証であることの方が重要。
- 複雑な依存関係を持っていても簡単にテストができる
- が、これは複雑な依存関係を要している設計に問題があって、その事実を見逃している可能性が高い。
- テストが失敗したときに問題の箇所の特定が容易
- が、十分な頻度でテスト実行してるなら問題の箇所はだいたい直近変更したところ。加えて、1箇所の変更で広い範囲のテストが落ちることが「依存グラフ上でテスト対象が重要な価値を持つ」という有用な情報を与えてくれる。
また、ロンドン学派は古典学派と比べてテストがテスト対象の内部的なコードと結びつきやすい。本書は古典学派推し。
統合テストについて
- ロンドン学派的には、実際の依存を使うなら全部統合テスト。
- 古典学派的には、次のいずれかが破られたテストを統合テストとみなす
- 1単位の振る舞いを検証すること
- 実行時間が短いこと
- 他のテストケースから隔離されていること
- たとえば?
- 共有依存をそのまま扱う(DBに実際に値いれる、とか)
- 実行順序に気をつけないといけない、アクセスの時間がかかって遅い
- テストケース内で複数の振る舞いを検証する
- 共有依存をそのまま扱う(DBに実際に値いれる、とか)
- E2Eは統合テストの極端な例
[1部 単体テストとは] 3章 単体テストの構造的解析
AAAパターン
- どういうもの?
- 準備 (Arrange), 実行 (Act), 確認 (Assert) の3フェーズ構造
- 準備 = テスト対象とその依存の状態を準備
- 実行 = テスト対象のメソッドを実行
- 確認 = 想定した結果であることを確認
- 確認フェーズを読みやすくするライブラリを使おう
- 準備 (Arrange), 実行 (Act), 確認 (Assert) の3フェーズ構造
- メリット
- テストケースに一貫した構造をもたせる
- アンチパターン
- 簡潔・実行時間が短い・理解が容易、を破る要素
- 例
- 複数のテストケースの混在。Arrange → Act → Assert → 別のAct → 別のAssert…
- テストケースに if 文などの分岐を混ぜること
- 各フェーズのサイズ
- 準備
- もっとも大きい。
- サイズを小さくする工夫の例
- 別のプライベートメソッドに切り出す、ファクトリクラスをつくる
- Object Mother パターン、Test Data Builder パターン
- 実行
- 基本、1行で済む。
- 1行を超す場合は設計を疑う。カプセル化に失敗している可能性がある。
- 確認
- 大きさは、「1単位の振る舞い」を検証できる程度。
- 肥大化してる場合は、コードの抽象化がうまくいってないことを疑う。
- 例: 全フィールドを個別比較してるなら、オブジェクト比較で済むようにする。
- 準備
- Tips
- 「こいつがテスト対象だな」と一目 で分かるようにする。
- 対象の変数命名に一貫性を持たせる
- 各フェーズを読みやすいように区切る
- 空白行を挟む、コメントを書く
- 「こいつがテスト対象だな」と一目 で分かるようにする。
xUnitについては割愛
Test Fixture をテストケース間で共有するには
- アンチパターン
- テストコンストラクタで一律用意
- 欠点
- テストケース間の結合を強める
- あるテストケース用にフィクスチャ準備処理を変更 → 別のテストケースに影響 (隔離の破れ)
- テストケースが読みづらくなる
- テストケースの理解のために、テストケース + コンストラクタを見ないといけない
- テストケース間の結合を強める
- 欠点
- テストコンストラクタで一律用意
- グッドプラクティス
- プライベートなファクトリメソッドを用意する
- テストケース間で共有しやすくするコツ
- メソッド名と引数を見るだけで何が用意されたか分かる
- 引数で条件の違うフィクスチャを用意できる
- テストケース間で共有しやすくするコツ
- プライベートなファクトリメソッドを用意する
テストメソッドの名前の付け方
- 指針
- 厳格なルールを設けないこと
- 命名規則で縛るより、自由な表現で分かりやすくする
- ドメイン知識のある非開発者が分かるようにすること
-
_
で単語を区切ること - 対象のメソッド名をテストメソッド名に含めない
- コードではなく振る舞いをテストしているため。メソッド名を変えるときにテストメソッドも変えなきゃいけない。
- should ではなく、事実を伝える
- 例: x_should_be_y → x_is_y
- 厳格なルールを設けないこと
パラメータ化テスト
- コード量と読みやすさのトレードオフ
- 読みやすさを維持するコツ
- 正常系と異常系を混ぜないこと
- 振る舞いが複雑でパラメータ化が大変なら、そもそもテストケースを分けること
[2部 単体テストとその価値] 4章 良い単体テストを構成する4本の柱
良い単体テストを構成する4本の柱
第1の柱: 退行 (regression) に対する保護
= バグが混入した際にテストが落ちる (偽陰性が少ない)
- どのくらい備えているか?の評価基準
- テスト時に実行されるプロダクションコードの…
- 量 (なるべく多くのコードが実行・検証されてること)
- 複雑さ・ドメイン重要性 (壊れたときの被害が大きい部分をカバーしてること)
- テスト時に実行されるプロダクションコードの…
第2の柱: リファクタリング耐性
= リファクタリングでテストが落ちない (偽陽性が少ない)
- これがないと何故困る?
- テスト結果の軽視・無視に繋がる
- リファクタリングが億劫になる
- どうすれば備わる?
- テスト対象を「内部的なコード」から「観察可能な振る舞い」にする
- 呼び出す側の目線で意味のある結果をテストする
- テスト対象を「内部的なコード」から「観察可能な振る舞い」にする
第3の柱: 迅速フィードバック
= 実行時間の短さ
- メリット
- テストケースの数を増やしやすい
- バグを素早く検出できる
第4の柱: 保守しやすさ
= 保守コスト
- どのくらい備えているか?の評価基準
- テストケースの理解の難しさ (理解のために読むコード量が少ない, etc…)
- テスト実行の難しさ (外部依存が少ない, etc…)
4本の柱の関係
- テストの正確性に関する柱 = 「退行に対する保護」「リファクタリング耐性」
- テスト結果の SN 比。
- 「退行に対する保護」 = 偽陰性の少なさ、「リファクタリング耐性」 = 偽陽性の少なさ
- 偽陰性はプロジェクト初期から継続して Fatal。偽陽性はプロジェクトの成長に伴って徐々に Fatal になる。
- 柱は掛け算
- ひとつでも欠くと途端に質が下がる
- かつ、すべての柱を MAX にはできない
- E2Eテスト = 「退行に対する保護」◎、「迅速なフィードバック」✕、「リファクタリング耐性」◎
- 取るに足らないテスト = 「退行に対する保護」✕、「迅速なフィードバック」◎、「リファクタリング耐性」◎
- 壊れやすいテスト = 「退行に対する保護」◎、「迅速なフィードバック」◎、「リファクタリング耐性」✕
- 「保守しやすさ」は独立。他の柱を担保しつつ追求する必要がある。
- ベストプラクティス
- 「リファクタリング耐性」と「保守しやすさ」は最大限備える
- 「退行に対する保護」と「迅速フィードバック」は天秤 → どうバランスをとる?
テストピラミッド
- 単体テスト → 統合テスト → E2Eテスト、の順に…
- テストケースの数が減る
- ユーザー視点に近づく
- 単体テスト側では「迅速フィードバック」、E2Eテスト側では「退行に対する保護」を優先する
ブラックボックステスト・ホワイトボックステスト
- 「リファクタリング耐性」確保のためにブラックボックステストを選択する