📖

【読書】Unit Testing Principles, Practices, and Patterns

2023/07/26に公開

https://www.amazon.co.jp/Unit-Testing-Principles-Practices-Patterns-ebook/dp/B09782L692/ref=sr_1_2?qid=1701049601&refinements=p_27%3AVladimir+Khorikov&s=digital-text&sr=1-2&text=Vladimir+Khorikov

感想

  • 「テストは分類器」であるととらえ、混同行列における偽陽性を取り除くことが、単体テストで注力すべき柱であるという視点が得られたのが大きい。偽陰性を出さないことも重要ではあるが、プロジェクトの持続的な成長の観点からは偽陽性を出さないようなテストを書くことが最も重要である、と意識するだけでもテストの書き方は変わる。どうすればそのようなテストを書けるのか?という疑問にも答えが示されている。
  • 数学的な背景のある著者は、プログラミングのガイドラインは数学的な定理と同様に第一原理から導出されるべきであるとの立場に立っており、本書は一貫して根本的な問いから徐々に論拠を構築して、最終的な結論に至るようなボトムアップ式の展開となっており、非常に理解しやすく腑落ちするところが多かった。
  • 単体テストや統合テストでは、何をモックして、どんなスコープで、どんな観点からテストするのが良いのか、その考え方を理解できた。また、テストにおいてもクリーンアーキテクチャや DI、ドメイン駆動設計などの知識が役に立つ、と改めて思った。

勉強になったポイントを以下にまとめてみた。質問形式にしてあります。

ユニットテストのゴールは?

ソフトウェアプロジェクトの持続的な成長を可能にすること。プロジェクトを成長させることは比較的簡単だが、長期間にわたって維持することは難しい。

The goal is to enable sustainable growth of the software project. The term sustainable is key. It’s quite easy to grow a project, especially when you start from scratch. It’s much harder to sustain this growth over time.

Untitled

ソフトウェアエントロピーとテストの関係は?

ソフトウェアエントロピー:開発速度が急激に低下すること。

This phenomenon of quickly decreasing development speed is also known as software entropy.

ソフトウエアを開発する=エントロピーが不可避的に増大する

In software, entropy manifests in the form of code that tends to deteriorate. Each time you change something in a code base, the amount of disorder in it, or entropy, increases.

テストは開発に伴うエントロピー増大を抑える。リグレッション(回帰=バグ)に対する safety net として機能する。

Tests help overturn this tendency. They act as a safety net—a tool that provides insurance against a vast majority of regressions.

コードは資産ではなく負債(貸借対照表の表裏)であり、テスト自体もまたコードであるので、エントロピー増大に寄与し、バグも発生する負債である。なので、必要以上のテストコードは不要

Code is a liability, not an asset. The more code you introduce, the more you extend the surface area for potential bugs in your software, and the higher the project’s upkeep cost. It’s always better to solve problems with as little code as possible.

Tests are code, too. You should view them as the part of your code base that aims at solving a particular problem: ensuring the application’s correctness.

いいテストの特徴は?

一言でいえば、開発サイクルに組み込まれていて、本質をついて無駄がなく、維持コストの低いテストコード。

A successful test suite has the following properties:

  1. It’s integrated into the development cycle.
  2. It targets only the most important parts of your code base.
  3. It provides maximum value with minimum maintenance costs.
  1. It targets only the most important parts of your code base.

    ドメインモデルにのみ集中し、テストする。以下のようなコードはテストしない(※コードは資産ではなく負債)

    • Infrastructure code(インフラ関連コード)
    • External services and dependencies, such as the database and third-party systems(外部プロセス依存)
    • Code that glues everything together(部品をつなぎ合わせるコード)
  2. It provides maximum value with minimum maintenance costs.

    ⇒  テストを最小労力で最大の効果を得る

    ※ ここが本書の核心であり、読む目的である、と書いてある。

unit test の定義は?Integration test/ e2e test との関係性は?

unit test とは
以下の 3 つをすべて備えたもの

  • Verifies a small piece of code (also known as a unit) : 少量のコード=unit を検証する。
  • Does it quickly: 実行が早い
  • And does it in an isolated manner.: 隔離された環境で実行される

Integration test とは
上の 3 つのうち、1 つでも満たさないものがある場合、Integration-test という。

例えば、

  • 2 つ以上のふるまい単位(two or more units of behavior)を確認しようとするテスト(1 の違反)
  • 遅い(2 の違反)
  • 共有 DB にアクセスして、他のテストと独立していないテスト(3 の違反)

e2e test とは
e2e テストは Integration test の部分集合で、通常、より多くの依存関係を含む。

End-to-end tests are a subset of Integration tests.

一般的には、Integration test は 1 つまたは 2 つのプロセス外の依存関係と連携し、e2e test は、すべてのプロセス外の依存関係、またはその大部分と連携する。e2e test はエンドユーザーの視点からシステムを検証する。e2e テストは最もコストが高いので、最後にやる。

Untitled

unit(=単体)と isolation(=隔離)の考え方の違いによる unit テストの学派を二つ挙げ、それぞれにおける Mock の考え方を述べよ。また、どちらの学派が unit test の目的に適うものか?

unit テストの学派とそれぞれの考え方

  1. ロンドン学派 (London school)

    • 「unit = A class」
    • SUT[1]Collaborator[2][3] から隔離する
    • 「テスト=クラス(部品)の検査」

      The London school views it as isolation of the system under test
      from its collaborators

    • 不変オブジェクト以外のすべて(=共有 or 可変オブジェクト)を Mock する(=Mockist)。この考えでいくと非常にテストの方針が単純となりわかりやすくなる。

      Untitled

  2. 古典学派(Classical school/Detroit)

    ※TDD が典型

    • 「unit = A class or a set of classes」

    • テスト自体を他のテストから隔離する

    • 「テスト=振る舞いの検査」

    • テスト自体を他のテストから隔離する、という思想なので、自然と他のテストに影響を及ぼす Shared dependency(共有依存)のみを Mock にする。

      the classical school views it as isolation of unit tests themselves from each other.

      Untitled

    2 つの学派の比較表

    Untitled

    どちらの学派がユニットテストのゴールに適うか?
    古典学派がよい。より品質の高いテストになる。テストの目的である、「持続的な成長」に適う。

    ⇒ Mock という行為は、実装の詳細(Imprementation detail)をテストに持ち込むことであり、観察可能なふるまい(Obserbable behavior)に焦点を当てた同一のテストが、実装詳細の変更により壊れやすく(=fragile)なる。つまり、ロンドン学派のテストは後述する Resistance to refactoring(リファクタリング耐性)が劣るため、避けるべきである。

良いユニットテスト 4 本の柱をそれぞれ一言でいうと?また、4 本の柱の間のトレードオフと優先順位の考え方について説明せよ。

良いユニットテスト 4 本の柱(the four pillars of a good unit test)について

A good unit test has the following four attributes:

  1. Protection against regressions
  2. Resistance to refactoring
  3. Fast feedback
  4. Maintainability
  1. Protection against regressions(退行への保護)
  • バグの検出率(感度、recall)

    これが低いとバグの見落とし(偽陰性/false nagative/第二種の過誤)が多くなり、テストの信頼性が低下する。

  1. Resistance to refactoring(リファクタリング耐性)
  • バグがないことの検出率(特異度、specificity)

    これが低いと、コードをリファクタリングした際に不要なアラーム(偽陽性/false positive/第一種の過誤)が多発し、テストの信頼性が低下する。(=Brittle test

  1. Fast feedback(素早いフィードバック)
  • 実行速度が速い
  1. Maintainability(保守性)
  • メンテナンス性が高い

4 本の柱の間のトレードオフと優先順位の考え方

第 2 の柱(リファクタリング耐性)は犠牲にできない[4] ため、テストスイートを堅牢にするために最優先するのは偽陽性(False Positives)を取り除くこと。また、第 4 の柱(保守性)は他の柱に影響しない。
よって、優先順位は第 2 の柱(リファクタリング耐性)と第 4 の柱(保守性)を最大化することとなり、トレードオフは第 1 の柱(退行への保護)と、第 3 の柱(素早いフィードバック)の間の二者択一問題に帰着する。

第 1 の柱(退行への保護)と、第 3 の柱(素早いフィードバック)の間のトレードオフのバランスの考え方

テスト・ピラミッドで占める層によって、どちらを優先するかが異なる。

  • エンド・ユーザーの視点に近く、テスト数の少ない E2E テストでは第 1 の柱(退行に対する保護)が優先される
  • ユーザー視点から離れてテスト数が増える Integration test や unit test では 第 3 の柱(素早いフィードバック)が優先される

Untitled

Untitled

プロジェクトのライフサイクルにおける偽陽性(False Positive)および偽陰性(False Negative)の影響度のダイナミクスについて

  • プロジェクト初期における影響:  FN > FP
  • プロジェクト成長期における影響:  FN ≒ FP

偽陰性(False Negative)はプロジェクトのライフサイクルにおいて一貫して大きな影響をもつ。偽陽性(False Positive)はプロジェクトが成長するにつれて大きな影響を及ぼすようになる。これは、プロジェクトが成長するにつれ、コードベースは複雑になり、リファクタリングの重要性が増すため。

Untitled

偽陽性(False Positive)が出る理由と対処について

偽陽性が出る理由
テストコードがプロダクションコードとより密に結合するほど FP がでる。

対処法
テストはテスト対象のメソッドの「観察可能な振る舞い(observable behavior)」、意味のある結果のみを、エンドユーザー視点で確認する必要がある。内部の実装詳細とカップリングしてはいけない。
ホワイトボックステストは網羅性が高いので第 1 の柱(退行への保護)には優れるが、実装詳細とカップリングするため、第 2 の柱(リファクタリング耐性)が劣る。そのためブラックボックステストをデフォルトとするべきである。

Untitled

テストダブルの 4 つのバリエーション(dummy, stub, spy, mock, fake)を 2 つに大別する分け方は?

テストダブルは、基本的にMockStubに分けられる。

  • Mock は外部へのコミュニケーションを模倣
  • Stub は内部へのコミュニケーションを模倣

command query separation (CQS)との関連性でいうと、Mock はコマンド(Command)にあたり、Stub はクエリ(Query)にあたる。

※ Stub が提供する結果を確認するのはアンチパターン(Stub はテスト対象ではない)

Untitled

単体テストの 3 つの手法と、その比較

  • Output-based testing(出力値ベース)
  • State-based testing(状態ベース)
  • Communication-based testing(コミュニケーションベース)

リファクタリング耐性の観点、および保守性の観点から、出力値ベースが最も優れるが、プロダクション・コードが関数型アーキテクチャで構成されている必要があり、すべてに適用できるわけではない。実際には出力値ベースと状態ベーステストを組み合わせて使用する

単体テストと Integration テストのバランスの考え方の原則

単体テストではドメインモデルのロジックの異常ケースを可能な限りテストする。

Integration テストは 1 つのハッピーパス(happy path)と、単体テストでは検証できない異常ケースをテストする

The ratio between unit and Integration tests can differ depending on the project’s specifics, but the general rule of thumb is the following: check as many of the business scenario’s edge cases as possible with unit tests; use Integration tests to cover one happy path, as well as any edge cases that can’t be covered by unit tests.

DEFINITION A happy path is a successful execution of a business scenario

Integration テストで Mock する対象は?単体テストでの Mock 対象との違いは?

Integration テストで Mock する対象
Integration テストでは Unmanaged dependencies[5] のみ Mock する。Managed dependencies[6] は Mock せず、real インスタンスをそのまま使う。

Untitled

単体テストでの Mock 対象との違い
古典学派(classical school)の単体テストでは共有依存(≒ 外部プロセス外依存)は Mock される。Integration テストでは外部プロセス依存のうち unmanaged な依存だけを Mock する。

e2e テストと Integration テストの違いは?

e2e テストは実装を終えたアプリケーションを「テスト環境にデプロイ」してテストする。Integration テストはインメモリでのテスト。また、e2e テストでは Mock を完全に排するが、Integration テストでは unmanaged なプロセス外依存を Mock する。

Untitled

Untitled

Integration テストにおける Mock のベストプラクティスを挙げよ

  1. Integration テストでは Unmanaged な外部プロセス依存のみ Mock する。そうしないとリファクタリング耐性を欠いた brittle test になる。

    ※単体テストの場合、共有依存のみ Mock する。

  2. Integration テストでは Managed な外部プロセス依存(例:DB)を Mock しないので、テストは順番に(sequentially)実行し、tear-down 処理をテスト毎に実行して、他の Integration テストと隔離する必要がある。

  3. Unmanaged な外部プロセス依存はインターフェースを定義し、実装を切り替えられるようにしておく。Mock する場合、Mock のインスタンスを注入してテストする。インターフェースはドメインモデルと外界の境界に定義し(DIP)、このような境界におけるシステムのふるまい(Observable behavior)のみ確認する

    ※ ドメインモデル内部での interactions は単体テストでのみ実施する。Integration テストにおいては実装の詳細となるので避ける(= brittle test を避ける)

  4. Mock の呼び出し回数を必ず確認する

  5. 3rd パーティ製ライブラリに対してはアダプターを作成し、ドメインの用語を使って必要な機能のみを公開するようなインターフェースを作成する。テスト時にはアダプターを Mock する。

脚注
  1. SUT: System Under Test、この場合、一つのクラス ↩︎

  2. Collaborator という言葉は、ロンドン学派において Mock する依存のことを指すらしい。この場合、共有依存と、(プライベート依存のうち)可変なオブジェクトを指す。

    Collaborator vs. dependency
    A collaborator is a dependency that is either shared or mutable.
    Untitled

    ↩︎
  3. 依存(Dependency)は、Shared/Private、out-of-process/in-process の二軸で分類できる。

    Shared dependency (共有依存)
    テスト間で共有され、互いの結果に影響を与える依存関係

    • DB 書き込み、file system 書き込み(out-of-process)
      など

    private dependency(プライベート依存)
    テスト間で共有されない依存関係

    • 静的な可変フィールド、シングルトンオブジェクト(in-process)
    • 静的な不変フィールド(=値オブジェクト)(in-process)
    • DB 読みとり、file system 読みとり、Docker コンテナ(out-of-process)
      など

    Out-of-process Dependency(プロセス外依存)
    アプリケーション実行プロセス外の依存関係。≒ 共有依存

    In-process Dependency(プロセス内依存)
    アプリケーション実行プロセス内の依存関係。

    Untitled

    ↩︎
  4. 第二の柱(リファクタリング耐性)は、テストケースに備えられるか否かの選択になることが多いため、犠牲にできない。第一、第三の柱は、この点で柔軟に対応できる。
    ※ 偽陽性(false positive)が多いテスト=Brittle test というので、Brittle test を避けるのが第一
    Untitled ↩︎

  5. Managed dependencies (管理下にあるプロセス外依存):テスト対象のアプリケーションからのみアクセスできるもの。interaction が外界から見えない。ex:データベース ↩︎

  6. Unmanaged dependencies (管理下にないプロセス外依存):interaction が外界から観測可能。ex: SMTP server and a message bus ↩︎

GitHubで編集を提案

Discussion