🍇

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

2023/02/05に公開

はじめに

昨年発売された『単体テストの考え方/使い方』を読んだのでその要点のまとめと感想です。

第1部 単体テストとは

第1章 なぜ、単体テストを行うのか?

  • 単体テストの目標は、ソフトウェア開発プロジェクトの成長を持続可能なものにすること。
    • 質の良いテストケースで構成されたテストスイート[1]があることで、プロダクションコードに変更を加えても退行が発生しないことに確信を持てるようになる。
    • 結果、機能追加やリファクタリングを簡単に行え、開発スピードを長く維持できる。

  • 単体テストを単に作成できるだけでなく、良い単体テストを作成できることが重要である。
    • 悪い単体テストを書いていると、開発にかかるコストが増えていき、テストがない場合と実質的に変わらなくなってしまう。

  • 優れたテストスイートの特徴
    • テストすることが開発サイクルの中に組み込まれている。
    • コードベースの特に重要な部分のみがテスト対象となっている。
      • 費やした時間に対して価値が最も効果的に返ってくるのはビジネスロジックを含むドメインモデルのコード
    • 最小限の保守コストで最大限の価値を生み出すようになっている。

第2章 単体テストとは何か?

  • 単体テストとして定義されるテストは、以下の3つの重要な性質をすべて備えている。
    • 「単体」と呼ばれる少量のコードを検証する。
    • 実行時間が短い。
    • 隔離された状態で実行される。

  • 「単体」と「隔離」の解釈の違いにより、2つの学派が存在する。
単体の意味 隔離対象 テストダブル[2]の置き換え対象
古典学派 1つの振る舞い テストケース 他のテストケースの実行に影響のある共有依存[3]
ロンドン学派 1つのクラス 単体 不変依存[4]を除く全ての依存
  • 著者は古典学派のスタイルを好んでいて、本書では古典学派の定義を採用している
    • テスト対象の焦点をクラスに当てるのは間違っていて、1単位の振る舞いに焦点を当てなければいけない。
    • また、ロンドン学派のスタイルだと単体テストがテスト対象の内部的なコードと密接に結びつく傾向があるため、賛同できない。

3章 単体テストの構造的解析

  • AAAパターンという単体テストの構造を用いることで、全てのテストケースに対して簡潔で統一された構造を持たせることができる。また、この構造に慣れることで読みやすさが向上し、保守コストを下げることにつながる。
    • 準備フェーズ(Arrange)フェーズ...ケースの事前条件を満たすように、テスト対象システムとその依存の状態を設定するフェーズ
    • 実行(Act)フェーズ...テスト対象の振る舞いを実行するフェーズ
    • 確認(Assert)フェーズ...実行した結果が想定した結果であることを確認するフェーズ

  • 1つのテストケースの中に同じフェーズが複数ある場合、多くのことを検証しようとしている。
    • 単体テストは1つの振る舞いを検証するものであるため、ケースを分割する必要がある。

  • 以下の指針を守ることで可読性のあるテストメソッド名をつけれるようになる。
    • 厳密な命名規則に縛られないようにする。
    • 問題領域に精通している非開発者に対しても、検証内容が伝わる名前をつける。
    • 英語の場合はアンダースコア(_)を使って単語を区切る。

第2部 単体テストとその価値

第4章 良い単体テストを構成する4本の柱

  • 良い単体テストを構成するものとして以下の4本の柱があり、これらの掛け算でテストの価値を評価できる。
    • 退行に対する保護
    • リファクタリングへの耐性
    • 迅速なフィードバック
    • 保守のしやすさ

  • 退行に対する保護とは、テストをすることでバグの存在をいかに検出できるのかを示す性質。
    • テストの際に実行されるプロダクションコードが多くなるほど多くのバグを検出できる。
    • ビジネス的に重要な機能ほどバグによって生じる被害も大きくなるため、コードの複雑さやドメインの重要性にも考慮する必要がある。

  • リファクタリングへの耐性とは、いかに偽陽性を生み出すことなく、どのくらいのプロダクションコードに対してリファクタリングを行えるかを示す性質。
    • 単体テストにおいて偽陽性とは嘘の警告のことであり、テスト対象のコードは正しく機能しているにかかわらず、テストが失敗することである。
    • 偽陽性はテストの信頼をなくす。結果、問題のあるコードが本番環境に持ち込まれたり、リファクタリングが敬遠されるようになる。
    • 偽陽性は、テストケースがテスト対象の内部的なコードと結びつくことで発生する。
    • テストの検証内容は、実装の詳細ではなく最終的に得られる結果である必要がある。

  • 迅速なフィードバックとはテストの実行時間がどのくらい短くなるのかに影響する性質。
    • 速やかにテストを行えるようになると、用意されるテストケースやテストの頻度が増える。
    • 最終的にはバグがすぐ検出されるようになり、バグの修正コストも減る。

  • 保守のしやすさはテストケースを理解することがどのくらい難しいか、テストを行うことがどのくらい難しいかという指標で評価できる。

第5章 モックの利用とテストの壊れやすさ

  • テストダブルとは、プロダクションコードに含まれず、テストしか使われない偽りの依存として表現される全てのものを包括的に意味するもの。
    • 大きくまとめるとモックとスタブの2つに分類できる。
    • モックは外部に向かうコミュニケーション(出力)を模倣し、検証するのに使われる。
    • スタブは内部に向かうコミュニケーション(入力)を模倣するのに使われる。

  • アプリケーションが行うコミュニケーションには以下の2種類がある。
    • クラス間で行われるシステム内コミュニケーション
    • 外部アプリケーションと行うシステム間コミュニケーション

  • システム内コミュニケーションは実装の詳細であり、システム外コミュニケーションはシステム全体の観察可能な振る舞いを形成する。
    • ただし、テスト対象のアプリケーションからしかアクセスされないデータベースなど、外部から観測できないプロセス外依存は実装の詳細として扱う。

  • モックを使っていいのはシステム間コミュニケーションの場合で、かつそのコミュニケーションが外部から観察できる場合である。

第6章 単体テストの3つの手法

  • 出力値ベーステスト
    • テスト対象のコードが返す結果だけを検証する。
    • テスト対象のコードが何も副作用を生み出さず、呼び出し元に返す戻り値しか処理の結果がないことが前提となる。

  • 状態ベーステスト
    • 検証する処理の実行が終わった後にテストの対象の状態を検証する。
    • ここでいう状態とは、テスト対象システムや協力者オブジェクト、データベースやファイルシステムなどのプロセス外依存の状態のことを指す。

  • コミュニケーションベーステスト
    • モックを用いてテスト対象システムとその協力オブジェクトとの間で行われるコミュニケーションを検証する。

  • 出力値ベーステスト、状態ベーステスト、コミュニケーションテストの順で、質の高いテストケースを作成できる。
    • 出力値ベーステストは、テストケースが実装の詳細と結びつくことがあまりないためリファクタリングへの耐性が自然と備わる。また小さくて簡潔なテストケースになるため保守もしやすくなる。ただし出力値ベーステストは数学的関数を用いて記述されたコードに対してのみ適用できる。
    • 全てのテストケースを出力値ベーステストにするのではなく、できるだけ多くのテストケースを出力値ベーステストにするようにすれば良い。

第7章 リファクタリングが必要なコードの識別

  • テストコードとプロダクションコードは本質的に影響しあうため、テストスイートの改善にはプロダクションコードの改善が必要である。

  • プロダクションコードはコードの複雑さやドメインにおける重要性の観点、協力オブジェクトの数の観点から次の4種類に分類できる。
協力オブジェクトの数: 少ない 協力オブジェクトの数: 多い
コードの複雑さ/ドメインにおける重要性: 低い 取るにとらないコード コントローラ
コードの複雑さ/ドメインにおける重要性: 高い ドメインモデル・アルゴリズム 過度に複雑なコード
  • 取るにとらないコードはテストする価値はない。
  • コントローラは結合テストでテストされるべき
  • ドメインモデル・アルゴリズムは単体テストに対する費用対効果が高い。
  • 過度に複雑なコードはドメインモデル・アルゴリズムとコントローラに分割するべき。

  • 質素なオブジェクト(Humble Object)と呼ばれる設計パターンにより過度に複雑なコードを分割できる。
    • 過度に複雑なコードは、ロジックとテストするのが難しい依存とが結びついている。過度に複雑なコードからロジックを抽出し、抽出したクラスを包み込む質素なクラスを作成する。作成した質素なクラスに対して、テストするのが難しい依存を結びつける。

  • 条件によって振る舞いが異なるロジックを扱う場合は、コントローラで決定を下す過程を細かく分割することで対応する。 コントローラの簡潔さより、ドメインモデルのテストしやすさとパフォーマンスの高さを優先するというトレードオフの結果である。

  • 抽象化する対象をテストするよりも、抽象化された結果をテストするほうが簡単である。
    • ドメインモデルに対する状態の変更はデータストレージへの状態の変更に対する抽象化であり、ドメインイベントはプロセス外依存への呼び出しに対する抽象化である。

第3部 統合テスト

第8章 なぜ統合テストを行うのか?

  • 統合テストとは、単体テストの条件を1つでも満たしていないテストのこと。
    • システムがプロセス外依存と統合した状態で意図したように機能するのかを検証する。
    • 4種類のプロダクションコードの中でコントローラーに分類されるコードを検証する。
    • 単体テストよりも優れた退行に対する保護とリファクタリングへの耐性を提供する。

  • 単体テストではビジネスシナリオにおける異常ケースを可能な限り多く扱うようにし、統合テストではシナリオごとに1,2件のハッピーケースと単体テストで扱えなかった異常ケースをできるだけ多く扱うようにする。
    • そうすることでシステム全体が正しく機能することに自信を持てるようになる。

  • プロセス外依存のうち、管理下にない依存に対してのみモックを使用する。
    • 管理下にある依存
      • テスト対象のアプリケーションが好きなようにすることができるプロセス外依存。
      • テスト対象のアプリケーションとのコミュニケーションを外部から見ることができない。
      • 実装の詳細
    • 管理下にない依存
      • テスト対象のアプリケーションが好きなようにすることができないプロセス外依存。
      • テスト対象のアプリケーションとのコミュニケーションを外部から見ることができる。
      • 観察可能な振る舞いの一部

第9章 モックのベスト・プラクティス

  • 外部との環境にもっとも近いコンポーネントをモックで置き換えるようにする。そうすることにより、退行に対する保護とリファクタリングの耐性の両方を強化することができる。

  • システムの境界にあるクラスをテストダブルに置き換えるのであれば、モックよりスパイの方が優れている。スパイであれば実行結果を確認するコードをそこに記述でき、複数のテストケースから利用できるようになるため、コード量が減り可読性が上がる。

  • モックを用いたテストでは、想定している呼び出しが行われていること、および想定していない呼び出しが行われていないことの両方を確認する必要がある。

第10章 データベースに対するテスト

  • 準備フェーズ、実行フェーズ、確認フェーズではそれぞれ個別のトランザクションを持つようにする。
    • 本番環境ではビジネスオペレーションごとに個別のトランザクションが開始されるはず。統合テストでもできるだけ同じように振る舞わせなくてはならない。

  • 統合テストではテストケースを1つずつ実行するべき。
    • 複数のテストケースを同時に実行するための労力に対する見返りが少ない。

  • 読み込みに対するテストは書き込みに対するテストよりも重要度は低い。
    • 複雑または重要な読み込みだけをテストする。

  • リポジトリを直接テストすることは保守コストを高くしてしまい、労力に見合った退行に対する保護を得られることは滅多にない。統合テストのシナリオの一部に含ませ間接的にテストするようにする。

第4部 単体テストのアンチパターン

第11章 単体テストのアンチパターン

  • プライベートなメソッドに対するテスト
    • 直接テストするのではなく、観察可能な振る舞いの一部に含めて間接的にテストする
    • 公開されたAPIからプライベートなメソッドを十分に検証できない場合、抽象化の欠落が起こっている可能性が高い。
    • 抽象化したものを別のクラスとして抽出し、そのクラスに対してテストをする。

  • プライベートな状態の公開
    • テストすることを目的にプライベートにすべき状態を公開するべきではない。
    • テストでは本番環境と同じ方法でテスト対象のコードとやり取りをしなくてはならない。

  • テストへのドメイン知識の漏洩
    • テストを作成する際、プロダクションコードのアルゴリズムやロジックの知識をテストに持ってきてはいけない。ブラックボックステストの観点でテストする。

  • プロダクションコードへの汚染
    • テストのためだけに必要なコードをプロダクションコードに加えてはいけない。

感想

テストをする目的や、何をテストするべきか、どうテストするべきかということを体系的に学ぶことができた。これらを知りたくて本を買ったので、満足の内容であった。

7章では、過度に複雑なコードをどう改善しテストできるようにするのかという過程が詳細に書かれていた。
ドメイン部分とプロセス外依存部分とを切り離し、それぞれを単体テストと統合テストで検証できるようにするという説明が個人的にわかりやすかった。また、これまで単一責務と聞くと、同一のレイヤー内での役割分担(ドメインクラスで各々どういう役割を持たせるかなど)のイメージを持っていたが、レイヤーを分けること自体もこれに当てはまるのだと改めて気づきであった。

同じような意味の内容が別の言葉で表現されている箇所があり、たまに混乱する部分もあったが、サンプルコードや図がたくさん乗っていて全体的に読みやすい内容となっていた。

気になっている方はぜひ読んでみてください。

脚注
  1. ソフトウェアテストの目的や対象ごとに複数のテストケースをまとめたもの。自動化テストにおいては、テストの実行単位となる。 ↩︎

  2. リリース対象のオブジェクトと同じような見た目と振る舞いを持っていながらも、複雑さを減少させて簡潔になることでテストを行いやすくするオブジェクトのこと。 ↩︎

  3. テストケース間で共有される依存のこと。共有依存を扱う複数のテストケースが同時に実行されてしまうと、お互いの検証に影響を与え、正しい結果を得られなくなってしまう。例としてデータベースやファイルシステムがあげられている。 ↩︎

  4. 値オブジェクトや値と同義。特徴として、個別の識別性を持たず、自身の内容によってのみ識別されるもの。リテラルや列挙型など。 ↩︎

Discussion