$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

優れたテストスイートの4本の柱を学ぶ - 「単体テストの考え方、使い方」を読んだ

良いテストケースの作成手法を学ぶ - 「はじめて学ぶソフトウェアのテスト技法」を読んだ - $shibayu36->blog;に引き続き、ソフトウェアテストの知識について言語化を進めたいと考え、「単体テストの考え方、使い方」を読んだ。

この本では優れたテストスイートの4本の柱を「退行に対する保護」「リファクタリングへの耐性」「迅速なフィードバック」「保守しやすさ」と定義し、これらの観点で優れたテストスイートを作る方法について教えてくれる。またこの4つの柱はトレードオフの関係にあるため、単体テスト・統合テスト・E2Eテストがそれぞれどの観点を重視すべきかなどについても言語化してくれている。

自分はこの本は非常に勉強になった。なぜなら単体テスト・統合テストの指針が明快に記述されていて理解しやすく、また行きすぎたテスタブルな設計を諌め、テストしやすいがシンプルな設計についても教えてくれるからだ。

この本の中で以下の部分が印象に残った。とくに何をモックすべきかという部分については非常に面白かった。

  • テストメソッド名の指針は、ドメインエキスパートにどういう検証をするのかが伝わるような名前を付けること
    • 検証内容が明確に分かる名前をテストメソッドにつける
    • 例) Delivery_with_a_past_date_is_invalid()。過去の日付が指定された配達は不正である
  • テストは1単位の観察可能な振る舞いを検証する
    • 観察可能な振る舞いとは、「クライアントが目標を達成するために使う公開された操作」「クライアントが目標を達成するために使う公開された状態」のどちらかのみ
    • 1単位の振る舞いなので、assertが1つである必要はない
  • 「退行に対する保護」を最大限に持つように、モックの適用は管理下にない依存のみにすべき
    • 管理下にない依存 = アプリケーションが好きなようにできない依存。管理下にない依存とのコミュニケーションは外部からも見られる。例: メールサービス
    • そのアプリケーションのみから使われるデータベースなどはとくにモックする必要はない。逆にモックしてしまうとテストが通っても本番で動く保証が少なくなってしまう
    • 管理下にない依存とアプリケーションの境界に近い場所をモックするほど、退行に対する保護を最大限に備えられる。できる限りシステムの境界に位置するものをモックする

読書メモ

- 単体テストで成し遂げたいのはプロジェクトの成長を持続可能なものにする 7
- コードは資産ではなく負債。コードが増えればバグが持ち込まれる経路が増え、維持コストが高くなる 10
- coverageが高いほど良いテストではない。coverageは悪いテストスイートであるかを判断するのみ 11
- 単体テストにおける少量のコードが意味するもの は、単一のクラス、単一のクラスのメソッド 37
- テスト分離の観点の違い 41 🌟
    - 気づき: 古典学派の共有依存のみの置き換えをすべきというのがこの本の趣旨
- 単体テストで複数のAAAがあるのはおかしい 61
    - 統合テストなら好ましい場合がある。実行時間の関係で
- メソッド名に結果も書いてあるけどなあ 63、74
- Actフェーズが一行を超える場合、きちんと設計されていない可能性 63
- 単体テストで検証するのは、1単位のコードではなく、1単位の振る舞い。なのでassertが一つである必要はない 65 🌟
- いわゆるfixture使うな理由がちゃんと書いてある 75
- 何を検証するのかを明確に説明する名前をテストメソッドにつける 77
    - テストメソッドをドメインエキスパートからもわかるように
- テストメソッド名の指針 79 ⭐️
    - 厳格な命名規則に縛られない
    - ドメインエキスパートに、どういう検証をするのかが伝わるような名前をつける
    - 気づき: BDD推奨っぽい 81
- パラメータ化テストはgoっぽい 83
    - パラメータ化テストは正常系と異常系を分けた方が良い 86
- 優れたテストスイートの特徴 95 🌟
    -  4本の柱
    - 退行に対する保護。バグに対する検知度
    - リファクタリングへの耐性
    - 迅速なフィードバック
    - 保守しやすさ
- 振る舞いをテストすることでリファクタリング耐性を上げる 98
- テストコードがテスト対象の実装の詳細を検証するのはダメ 104
    - 最終的な結果を確認する 106
    - 役割の違いの良い図 ⭐️
- モックとスタブは外部に向かうコミュニケーションの模倣か、内部に向かうコミュニケーションの模倣か 133
    - この辺りにモックとスタブの違いが細かく記されている
- スタブのやり取りを検証しない 135
- モックとスタブの両方の性質を持ったテスト・ダブル 137
    - コマンドクエリ分離とモックスタブ
- 気づき: テストケースを実装の詳細と結びつけないことがとにかく大事。何度も言っている 140
- テストすべき観察可能な振る舞いとは、次のどちらか 141 ⭐️
    - クライアントが目標を達成するために使う公開された操作
    - クライアントが目標を達成するために使う公開された状態
- 理想とすべきAPIの設計は、いかなる目標であれ、1つの操作で目標を達成できる 146
    - 気づき: これは確かにそういう印象あり
- 気づき: テストの中で置き換える依存かどうかの判定は、複数のテストケースをランダム・同時に実行した時に落ちうるか 162
- 気づき: 置き換える必要のないプロセス外依存の理由について知れば、データベースでもモック化しない理由がわかる 162
- 単体テストの3つの手法 ⭐️ 168
    - 出力値ベース・テスト:戻り値を確認する
        - 副作用がない場合のみ適用できる
    - 状態ベース・テスト:状態を確認する
        - 状態とは、テスト対象システムの状態、協力者オブジェクトの状態、データベースやファイルシステムなどのプロセス外依存の状態 170
    - コミュニケーション・ベース・テスト:オブジェクト間のやりとりを確認する
- 単体テスト手法と、リファクタリング耐性・保守性の関係性 179
- 隠れた入出力の例 183
    - 隠れた出力:副作用、例外
    - 隠れた入力:内部もしくは外部の状態への参照
- 気づき: 関数型アーキテクチャの場合、出力値ベース・テストがしやすいのはその通り 189
    - 気づき: Goについては例外を基本使わないので、隠れた入出力のうちの一部が起こらない。そのためパラメータ化テストが基本になったのかも知れない
- 4種類のプロダクション・コード。ドメインにおける重要性と協力者オブジェクトの数で分類 217
    - コントローラ = 複数のコンポーネントが適切に連携できるような調整をする
    - 過度に複雑なコードは、ドメイン・モデルとコントローラに分割するほうが良い 218
- 質素なオブジェクトを作り、テストしづらい依存とロジックと分離する 220
- 事前条件についてテストするのは、事前条件がドメインにとって重要な時だけ 239
    - 従業員の数がマイナスにならない例は重要な事前条件、data.Length >= 3みたいなやつはそうではない
- 単体テストと統合テストのテストしたい領域の違い 263
- プロセス外依存をモックに置き換える基準 269 🌟
    - 管理下にある依存ならモックしない、管理下にない依存ならモックする
    - 管理下にある依存 = アプリケーションが好きにできるプロセス外依存
        - 例: そのアプリしか参照しないデータベース
    - 管理下にない依存 = アプリケーションが好きなようにできない依存。管理下にない依存とのコミュニケーションは外部からも見れる
        - 例: メール・サービス
- 統合テストの指針は、すべてのプロセス外依存を経由してビジネスシナリオを正常に終わらせるハッピーパスと、単体テストで検証できない異常系 275 🌟
- 実装クラスが一つしかない場合、プロセス外依存をモックにする必要がない限りはインターフェースは必要ない 282
- バックエンドのシステムは大抵のケースでドメイン層、アプリケーションサービス層、インフラ層の3つで十分 286
- 循環参照を防ぐために、ドメインからは値を返し、上の層で調整するやり方がある 289
    - CheckOutServiceはReportオブジェクトを返し、実際にReportをするのは上の層
    - 気づき: これはミスにつながる設計だと思うし、本当にいいのかなあという気はする
- 管理下にない依存とアプリケーションの境界に近い場所をモックするほど、退行に対する保護を最大限に備えられる 313 🌟
- モックのベストプラクティス 320
    - モックの適用は管理下にない依存のみに
    - モックの置き換え対象はシステムの境界に位置するものに
    - モックの利用は統合テストに限定
    - モックに対して行われた呼び出しの回数を常に確認
        - 想定する呼び出し、想定していない呼び出しがないことの二つを検証するため
    - モックの対象になる型は自身のプロジェクトが所有する型のみに
        - 気づき: これが良い理由があんまりわからんな〜
        - 気づき: 必要な機能のみを公開できると良いので、アダプタがあると便利というのは分かる