📖

「単体テストの考え方/使い方」読書メモ

2024/08/16に公開

書籍

「単体テストの考え方/使い方」
著者:Vladimir Khorikov
訳:須田 智之
出版年:2022年
出版社:マイナビ出版
リンク:https://www.amazon.co.jp/単体テストの考え方-使い方-Vladimir-Khorikov/dp/4839981728

概要

以前会社の目標のために「単体テストの考え方/使い方」を読み、社内で共有しました。
以下の内容は、社内で共有する際のアウトプットとなります。
また、現場ではLaravelとPHPUnitを使っているため、ときどきPHPUnitに置き換えて個人的な注釈を書いています。

読んだ背景

現場にテスト観点表を作るために読んでみました。
結合テストの仕様書フォーマットはあったものの、単体テストの観点がかなりバラバラになっていたので、観点の勉強のためでもありました。

感想

テストを書くだけではなく、「そもそもテストが書きにくかったらアーキテクチャの修正を考える」という考え方が面白かったです。
既に現場ではかなりの数の単体テストがあり、一から変えていくのは難しいですが、新規実装時には本書の内容を活かしていきたいと思いました。

読書メモ

第1章

1.1 単体テストの現状

膨大な数の単体テストを用意することで必ずしも望ましい結果を得られるとは限らない。
悪い単体テストを書いてしまうことにより、プロジェクトのバグを引き起こす可能性がある。

良いテストを書くことが、プロジェクトの成功の鍵となる。

1.2 なぜ単体テストを行うのか

ソフトウェアを「持続可能」にするため。

単体テストがない場合、プロジェクトの最初は単体テストを作る工数がないため成長速度は単体テストがある場合よりも早くなる。
しかし、プロジェクトが進むにつれて単体テストがないことによる修正や考慮する事項が多くなるため、単体テストがない場合の工数は2次関数的に増加する。

結果として、どこかのタイミングで単体テストある場合の方が工数が少なくなる。

単体テストがない場合工数が急激に増加する現象を「ソフトウェア・エントロピー」という。

ただ、質の悪い単体テストが多い場合、単体テストのリファクタリングを行う必要があるため、むしろマイナスになってしまうこともある。
基本的に、コードは資産ではなく負債という考えを持つべきである。

1.3 網羅率とテスト・スイートの質との関係

網羅率は、「テストにおいて実行したコードの行数/全てのコードの行数」で表される。

ただ、以下のような場合ロジックは同じであるのに網羅率は異なるという問題があるため、網羅率だけを良いテストの指標にするべきではない。

public function isStringLong(string input)
{
    if (strlen(input) > 5) {
        return true
    } else {
        return false; 
    }
}

public function isOverFive()
{
    $this->assertTrue(isStringLong('abcdef'));
}

public function isStringLong(string input)
{
    return strlen(input) > 5 ?: false;
}

public function isOverFive()
{
    $this->assertTrue(isStringLong('abcdef'));
}

①の場合は6行目を網羅できていないが、②の場合は全て網羅しており、網羅率に違いが出てしまう。
上記のような場合を避けるために、「分岐網羅率」を利用すべきである。

分岐網羅率は「経由された経路の数/分岐経路の総数」で算出される。
分岐網羅率であれば、①②どちらも同じ割合であると判断できる。

しかし、どちらにしても網羅率だけを見た場合、網羅をしているassertメソッドを使っていないテストや、以下のようなテストを防ぐことができない。

public function isStringLong(string input)
{
  $result = strlen(input) > 5 ?: false;
  wasStringLong($result);
  return $result;
}

public function isOverFive()
{
  $this->assertSame(false, isStringLong('abcdef'));
}

上記のような場合、6行目の結果が確認できていないが、分岐網羅率・網羅率は100%となる。

また、Laravelのようなフレームワークや、ライブラリを使った場合のコアファイルの中までテストはできないため、ライブラリを使っている時点で本当の意味での網羅率を知ることはできない。

そのため網羅率は、「60%だったらテストが不足しているかもしれない」という目安にとどめ、テスト観点自体を見てテストの価値を高める方が良い。

1.4 何がテスト・スイートの質を良くするのか?

テスト・スイートとは、類似するテストケースをひとくくりにし、テストの実行単位である。(本書には記載していなかったため、以下サイトを参照しました。)

https://www.ibm.com/docs/ja/engineering-lifecycle-management-suite/lifecycle-management/7.0.2?topic=scripts-test-cases-test-suites

テストの質を良くするには、以下のポイントが重要である。

  • テストすることが開発サイクルの中に組み込まれている。
    →「実装」の中に単体テスト実装がタスクとして組み込まれているかどうか。

  • コードベースの特に重要な部分のみがテスト対象となっている。
    →インフラ関連のコードや、ライブラリのロジックなどは単体テストしない。

  • 最小限の保守コストで最大限の価値を生み出すようになっている。
    →「価値のあるテスト」を作成できる能力がある。

1.5 本書から学べること

開発を経験する過程で誰もが自然と身につけていることを、「なぜその方法が有用なのか」について学ぶことができる。

第2章

2.1 単体テストの定義

単体テストには以下の特徴がある。

  • 「単体(Unit)」と呼ばれる少量のコードを検証する。
  • 実行時間が短い。
  • 隔離された状態で実行される。

3つ目の性質については、単体テストにおけるロンドン学派・古典学派とは2つの学派によって意見が分かれる。

主な違いは以下の通りである。

  • ロンドン学派
    →「コード」を隔離し、モックを多用する。

  • 古典学派
    →「テストケース」を隔離し、モックをあまり使わない。(PHPUnitはこっち)
     プライベート依存(ローカルのDBのような)で、共有依存(STG環境のDBなど、他に影響があるもの)に関するものでなければ、単体テストと認識する。

テスト・ダブル(≒モック)を使うメリットとしては、以下のものがある。

  • テスト・ダブルを利用することで、アプリケーションコードのみを検証することができる。
  • 複雑な実装の場合、モックを用いない場合循環依存が起きる可能性がある。
  • 早い(メモリをあまり使わないため)

2.2 古典学派、ロンドン学派が考える単体テスト

それぞれ、以下の違いがある。

学派 隔離対象 単体の意味 テスト・ダブルの置き換え対象
ロンドン学派 単体 1つのクラス 普遍依存を除く全ての依存
古典学派 テストケース 1つのクラス、もしくは、同じ目的を達成するためのクラスの1グループ 共有依存

協力者オブジェクトとは、共有依存または可変依存となるものである。
DBアクセスするようなクラスの場合はDBが共有依存であるため、そのクラスが協力者オブジェクトとなる。

2.3 単体テストにおける古典学派とロンドン学派の違い

ロンドン学派の単体テストのメリットは以下の通りです。

  • より細かな粒度で検証ができる。
    →テストコードが何をテストしているのか分かりやすくなる。

  • 依存関係が複雑になっていても簡単にテストすることができる。
    →継承やインスタンス作成などが複雑になった場合、テストデータを作成しやすい。

  • テストが失敗した際、どの機能に問題があったのかを正確に見つけられるようになる。
    →テスト・ダブルを使わない場合、テスト対象のコードではなく、コードが呼び出している依存先に不具合があったら発見しにくい。

ロンドン学派の単体テストはアプリケーションコードに基づいて作られる。
そのため、実装仕様を深く理解している必要があり、アプリケーションコードと結びつき過ぎてしまう。

一方、古典学派の単体テストはテストコードとアプリケーションコードがぶんりしているため、テストコードを先に作る(TDD)ことができる。

2.4 古典学派およびロンドン学派における統合(integration)テスト

ロンドン学派では、協力者オブジェクトを使うテスト全てが統合テストとなる。

古典学派では、以下の特徴に当てはまらないテストを統合テストと認識する。

  • 1単位の振る舞い(a unit of behavior)を検証すること。
  • 実行時間が短いこと。
  • 他のテスト・ケースから隔離された状態で実行されること。

E2Eテストも統合テストに含まれる。

第3章

3.1 単体テストの構造

単体テストの構造には、以下のようなAAAパターンがある。

  • Arrange(準備)
    →テストケースの事前条件を満たすように依存関係を設定するフェーズ

  • Act(実行)
    →テスト対象システムのメソッドを呼び出し、テスト対象の振る舞いを実行するフェーズ

  • Assert(確認)
    →実行結果が想定通りか確認するフェーズ

2.1にて記載した通り、1つの単体テストが重くならないようにする必要がある。
単体テストではif文をなるべく使わず、使う必要がある場合はテストを分割する。

各フェーズの長さは、準備フェーズが一番長く、実行フェーズは1行、確認フェーズは1単位の振る舞いのみを確認しているかが目安となる。

それぞれのフェーズがわかりやすいよう、空行で区切るか、どのフェーズかをコメントに残しておいた方が良い。

3.2 単体テストのフレームワークについて

フレームワークの選定は、「各テストケースはプロダクション・コードが解決しようとしている物語(story)について語るべき」である。

3.3 テスト・ケース間で共有するテスト・フィクスチャ(test fixture)

テスト・フィクスチャとは、テストを実施する際に使われるオブジェクトのことを指し、これらのオブジェクトはテスト実行前に決められた状態に毎回なっている必要がある。

コンストラクタを用いた場合、何のデータを作っているか可読性がおちるため、なるべくプライベート関数を作り、それを各テストケースで呼び出すようにする。

3.4 単体テストでの名前の付け方

テストメソッドに名前をつけるときの指針は、以下のものが望ましい

  • 厳格な命名規則に縛られないようにする。
    →厳格な命名規則には限界がある。

  • 問題領域のことに精通している非開発者に対してどのような検証をするのかが伝わるような名前をつける。

  • アンダースコアを使って単語を区切るようにする。
    →読みやすくなるため。

テスト対象のメソッド名をテスト名にした場合、メソッドの名前が変わる度にテストコードも変える必要があるため、なるべく避ける。

3.5 パラメータ化テストへのリファクタリング

パラメータ化(PHPUnitならdataProvider)すると、記述するコードを減らせるメリットがあるが、何のテストをしているかわかりにくくなるデメリットがある。

3.6 確認(Assert)フェーズの読みやすさの改善

確認するためのメソッドの英語を修正してくれるライブラリなどを用いると、後で見直しやすくなる。

第4章

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

良い単体テストを構成するものとして、以下の4本の柱がある。

  • 退行(regression)に対する保護
  • リファクタリングへの耐性
  • 迅速なフィードバック
  • 保守のしやすさ

退行(regression)に対する保護

コードは資産ではなく負債であり、コードの量が増えれば増えるほどバグが発生しやすくなる。
複雑なコードやコードのドメインの重要性、テスト時に実行されるコードの量を見ると退行に対する保護がどれくらいできているかを判断することができる。

また、退行を回避するためには、フレームワークやライブラリの考慮も必要であり、外部の依存に関してもテストの範囲に含める必要がある。

リファクタリングへの耐性

悪いテストコードでは、リファクタリングをした際に意図している挙動にも関わらず、テストが失敗してしまうことがある。(偽陽性)

偽陽性の影響で、テストが失敗することが当たり前になった場合、開発者がテストを重視しなくなり、リファクタリングを避けてコードの変更をなるべく行わないことに注力するようになってしまう。

偽陽性を引き起こさないためにはテストコードにて、コードのプロセスに着目するのではなく、結果のみにフォーカスする必要がある。

テストコードが対象となるコードと深く結びつくとリファクタリングへの耐性がなくなる。

4.2 退行(regression)に対する保護とリファクタリングへの耐性との関係

退行に対する保護とリファクタリングへの耐性の関係は両方ともテスト・スイートの正確性を向上させることに貢献する。

お互いの関係性は以下の図で表せる。

テストの正確性 実際の振る舞い
テストの結果 成功 真陰性(正しい) 偽陰性(退行に対する保護)
テストの結果 失敗 偽陽性(リファクタリングへの耐性) 真陽性(正しい)

偽陰性の発生は、「テストをすることでどのくらいのバグを検出できるのか?」という観点によって抑えられる。

偽陽性の発生は、「テストをすることでバグがないことをどのくらい示せるのか?」という観点によって抑えられる。

偽陰性が発生することは、本番でのバグ見逃しにつながるため直ちに影響が出るが、偽陽性の場合、直ちには影響が出ず、プロジェクトの成長に伴って影響が大きくなる。

偽陽性は直ちに影響を及ぼさないために、多くの開発者にとって問題とは考えられていないが、プロジェクトが進むにつれて影響も大きくなるため、偽陰性と同じくらい気をつける必要がある。

4.3 迅速なフィードバックと保守のしやすさ

  • 迅速なフィードバック
    →テストが早ければ改善までの時間も早くなるが、テストが遅ければバグがコードに潜む時間が長くなり、改善までの時間も長くなってしまう。

  • 保守のしやすさ
    →保守のしやすさは、以下の2つの観点から把握できる。

テストケースを理解することがどのくらい難しいのか?
 →テストケース1つあたりの行数を少なくすることでテストの理解を簡単にできる。
 テストの品質はプロダクションコードと同じくらい重要であるため、テストコードの作成に手を抜いてはいけない。

テストを行うことがどのくらい難しいのか?
 →テストを行うにあたり、プロセス外依存が必要であれば、ネットワークの問題に目を向けたりするなど、より多くのことに時間を消費することとなる。

4.4 理想的なテストの探求

上記の、テストの4本の柱を意識し、以下の計算式の結果が1に近いほど良い単体テストと言える。

テスト・ケースの価値 = [0…1] * [0…1] * [0…1] * [0…1]

複数の柱を満たすが1つの柱を全く満たさないテストの例として、E2Eテストや、取るに足らないテストがある。

E2Eテストは、外部サービスなど全てを利用してテストするため、退行に対する保護とリファクタリングへの耐性はあるが、迅速なフィードバックという点では0となる。
1行で完結するような取るに足らないテストは、偽陽性や実行時間の短さなどは満たしているものの。退行に対する保護という点では0となる。

また、結果(what)を検証するのではなく、どのように(how)ということを検証するテストはリファクタリングへの耐性という点で0となる。

バランスをどのようにとるかについては、以下の方法がよい。

  • 「リファクタリングへの耐性」を最大限にする。
  • 「退行に対する保護」と「迅速なフィードバック」の間でトレードオフを行う。
  • 「保守のしやすさ」を最大限にする。

4.5 ソフトウェア・テストにおけるよく知られた概念

テスト・ピラミッドは以下のようになっていると良い。

E2Eが最もユーザーに近い場所でのテストである。

そのため、退行に対する保護はE2Eテストにてより重視し、単体テストは迅速なフィードバックをより重視するのが良い。

しかし、どの層もリファクタリングへの耐性は持っている必要がある。
テストには大きく分けてブラックボックステストとホワイトボックステストが存在する。

それぞれ優れているのは以下の点である。

手法 退行に対する保護 リファクタリングへの耐性
ホワイトボックス 優れている 劣っている
ブラックボックス 劣っている 優れている

上記の通り、リファクタリングへの耐性は最重要となるため、基本的にはブラックボックステストを選択すべきである。

第5章

5.1 モックとスタブの違い

モックはテスト対象システムとその協力者オブジェクトとのやり取りを検証するのに使われるテスト・ダブルである。

テスト・ダブルにはモックとは別にスタブが存在する。

(以下参照)スタブは、あくまで テストを効率よく進めるためのツールなのに対し、モックはテストケースの一部のようなものといえる。
https://service.shiftinc.jp/column/8057/

モックはテスト対象システムからその依存に向かって行われる外部に向かう検証である。
反対に、スタブは依存からテスト対象システムに向かって行われる内部に向かう検証である。

スタブはテスト対象システムと依存の関係を模倣するだけなのに対し、モックは模倣して検証するという違いもある。

スタブに対しては、テスト対象システムと依存のいかなる関係も検証してはならず、過剰検証となる可能性がある。

モックとスタブは、コマンドとクエリのような関係である。

  • モックは実行すると副作用を起こし、戻り値を返す。
  • スタブは実行すると副作用を起こさずに戻り値を返す。

5.2 観察可能な振る舞いと実装の詳細

観察可能な振る舞いと内部的な実装の詳細はニュアンスの違いとなる。

テスト対象コードがシステムの観察可能な振る舞いの一部となるには、以下のいずれか当てはまる必要がある。

  • クライアントが目標を達成するために使う公開された操作
    →操作とは、計算したり副作用を起こすメソッドを指す。

  • クライアントが目標を達成するために使う公開された状態
    →状態とは、システムの現時点でのコンディションを指す。

上記に該当しないものが実装の詳細となる。

API実装の際は、設計の段階からそのAPIが「観察可能な振る舞い」なのか「実装の詳細」なのかを判断し、適切にカプセル化する必要がある。

適切にカプセル化すると、間違ったことをする可能性を取り除くことができるため、良い単体テスト作成につながる。

以下のように、観察可能な振る舞いか実装の詳細かを見て、公開非公開を判断する必要がある。

観察可能な振る舞い 実装の詳細
公開 すべき すべきではない
プライベート 該当なし すべき

5.3 モックの利用とテストの壊れやすさとの関係

一般的なアプリケーションは、ドメイン層とアプリケーションビジネス層に分かれる。

ドメイン層には、アプリケーションにとって不可欠なビジネス・ロジックが含まれる。

アプリケーション・サービス層はドメイン層の周りに位置し、REST APIを受け取った場合はまず最初にアプリケーション・サービス層に到達する。

このようなアプリケーション・サービス層とドメイン層を図で表すのに六角形がよく用いられる。
六角形で表現されたアプリケーションが組み合わさっていくことを表したものを、ヘキサゴナル・アーキテクチャという(下図)。

ヘキサゴナル・アーキテクチャには次の3つの特徴がある。

  • ドメイン層とアプリケーション・サービス層と関心の分離
    →ドメイン層ではビジネス・ロジックを記載し、その他の責任は担わないようにする。
     アプリケーション・サービス層は、ドメイン層と外部アプリケーションとのやり取りについてのみ責任を追うようにする。

  • アプリケーション内でのコミュニケーション
    →依存の流れは、アプリケーション・サービス層からドメイン層への一方向となる。
     ドメイン層からアプリケーション・サービス層を知ることはできないため、ドメイン層は外部から隔離される。

  • 外部アプリケーションとのコミュニケーション
    →外部アプリケーションがテスト対象システムに何らかの操作を行う場合、アプリケーション・サービス層を通るため、外部アプリケーションはドメイン層に直接アクセスできないようにしている。

コミュニケーションには以下の2種類がある。

  • システム内コミュニケーション
    →アプリケーション内のクラス同士のコミュニケーション

  • システム間コミュニケーション
    →外部アプリケーションとのコミュニケーションシステム内アプリケーションのテストに、モックを使うとテストが壊れやすくなることが多い。
    モックはシステム間アプリケーションのテストに使用するとよい。

5.4 振り返り:単体テストの古典学派とロンドン学派の違い

モックを無差別に使用した場合、前述の通りリファクタリングへの耐性がなくなるため、テストが壊れやすくなる。

プロセス外依存であっても、テスト対象のアプリケーション以外にプロセス外依存にアクセスする方法がない場合、コミュニケーションは観察可能な振る舞いではないため、モックは必要ない。

第6章

6.1 単体テストの3つの方法

単体テストには以下の3つの手法がある。

  • 出力値ベース・テスト
  • 状態ベース・テスト
  • コミュニケーション・ベース・テスト

出力値ベーステストとは、テスト対象のコードに引数を渡した後、戻り値を検証するテストである。

(PHPUnitなら、以下のようなものが該当する。)

     $response = $this->get(XXXXController@index);
     $response->assertSuccessful();

状態ベーステストとは、検証する処理の実行が終わった後にテスト対象の状態を検証するものである。
(PHPUnitなら、以下のようなものが該当する。)

    $response = $this->get(XXXXController@index);
    $this->assertDatabaseHas($response->json('data'));

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

古典学派では状態ベース・テストを好むのに対し、ロンドン学派はコミュニケーション・ベース・テストの方を好む。
出力値ベーステストはどちらでも使われる。

6.2 単体テストの3つの手法の比較

退行に対する保護、迅速なフィードバックについて備わっているかどうかは、
単体テストの手法ではなく以下の3つの要素によって変わってくる。

  • テスト時に実行されるコードの量
  • コードの複雑さ
  • ドメインにおける重要性

リファクタリングへの耐性については、3つの手法によってどれくらい備わっているかが変わってくる。

偽陽性を最も抑えられるのは出力値ベース・テストである。
理由は、テスト対象ケースが見るのはテスト対象メソッドのみであり、実装の詳細と結びつく唯一のケースは、テスト対象メソッド自体が実装の詳細である場合のみであるためである。

状態ベース・テストについては、検証にあたり多くのAPIと結びつくこととなり、
実装の詳細を漏洩するコードとも結びつく可能性があるため、出力値ベース・テストよりも偽陽性の可能性が高い。

コミュニケーション・ベース・テストでは、モックやスタブを用いるためテストが壊れやすく、偽陽性となる可能性が最も高くなる。
しかし、適切なカプセル化や観察可能な振る舞いのみをテストするようにすれば、偽陽性の発生を抑えることはできる。

保守のしやすさについても、3つの手法によってどれくらい備わっているかが変わってくる。
保守のしやすさの向上に関しては、テスト手法の選択以外に開発者ができることはあまりなく、以下の2点によってどのくらい備わっているかを判断できる。

  • テスト・ケースを理解することがどのくらい難しいのか?
    →度合いはテスト・ケースのコード量によって変わる。

  • テスト・ケースを実施することがどのくらい難しいか?
    →度合いはテスト・ケースで直接扱うプロセス外依存の数によって変わる。

保守のしやすさについて、出力値ベース・テストが最も保守のしやすいテストケースを作成できる。

出力値ベースであれば、テストケースで行う作業は入力値(引数)をメソッドに渡すことと、その結果を検証することのみであり、多くの場合数行のテストケースで済むはずである。

状態ベース・テストにおいては、確認項目が出力値ベースよりも多くなるため、必然的にコードの量は多くなる。

コミュニケーション・ベース・テストは、テスト・ダブルの準備も必要であり、モックの連鎖も発生する可能性があるため、保守が他に比べてさらに難しくなる。

6.3 関数型アーキテクチャについて

出力値ベース・テストを行うにはテスト対象コードが関数型である必要がある。
関数は、メソッド名・引数・戻り値の方で構成されるメソッド・シグネチャに明示される。
関数型アーキテクチャの場合、出力値ベーステストを採用できるため保守しやすくできる。

一方、以下のような隠れた入出力が存在する場合、保守がしにくくなる。

  • 副作用
    →オブジェクトの状態を変更したり、ディスク上のファイルを書き換えるなどがあり、隠れた出力となる。

  • 例外
    →例外がスローされる場合、呼び出しのどこかでキャッチされることとなるため、隠れた出力となる。

  • 内部もしくは外部の状態への参照
    →DBの値やCarbon::nowのような値を入力する場合、すべて定義されているものではないため、隠れた入力となる。

有用なテクニックとして、メソッドを呼び出している部分を値に置き換えて振る舞いを検証する方法があり、その能力を参照透過性という。

関数型アーキテクチャの目的は、副作用をなくすことではなく副作用とビジネスロジックを分離することにある。
関数型アーキテクチャでは、副作用をビジネス・オペレーションの最初や最後に持っていくことで、副作用を分離する。
ビジネスロジックと副作用の分離は以下の2種類のコードに分離することで行われる。

  • 決定を下すコード
    →副作用を起こさないコードのため、数学的関数を使って書くことができる。

  • 決定に基づくアクションを実行するコード
    →数学的関数によって下された決定を観察可能な振る舞いの一部に変換する。
      関数型アーキテクチャとヘキサゴナル・アーキテクチャの違いには、副作用への扱いがある。

関数型アーキテクチャでは、すべての副作用を関数的核の外に出すが、
ヘキサゴナル・アーキテクチャでは、ドメイン層内に限定される限り副作用を起こすことが許されている。

6.4 関数型アーキテクチャおよび出力値ベース・テストへの移行

テスト・ケースがプロセス外依存と深く結びついた場合、手間と時間がかかってしまうため、モックに置き換えることで問題を解決することが出来る。

関数型アーキテクチャでは、副作用の分離が求められるため、ビジネスロジックの責任のみを負うクラスを作ることで副作用を分離する。

そのようにして関数型アーキテクチャを取り入れたのちに出力値ベース・テストへ修正することでテストの質を高めることが出来る。

6.5 関数型アーキテクチャの欠点

保守のしやすさによる利点が、パフォーマンス悪化やコードの肥大化につながることがある。
また、現実として関数を呼び出すまでに全ての情報がそろわない場合もあり、関数の中の処理でプロセス外依存からデータを取得することも考えられる。

関数型アーキテクチャにすることは、プロダクション・コードにてプロセス外依存へのアクセスが導入前よりも増えることとなり、パフォーマンスが低下する可能性もある。
関数型アーキテクチャまたは伝統的なアーキテクチャを選ぶかどうかは、パフォーマンスとコードの保守のしやすさどちらをとるかのトレードオフとなる。

関数型アーキテクチャではコードの肥大化がデメリットとしてあり、そのシステムがどれほど重要なのか・システムがどれほど複雑なのかを考えて取り入れる必要がある。

出力値ベース・テスト以外のテストを用いること自体に問題はなく、できるだけ多くのテストケースを出力値ベース・テストにすることが重要である。

第7章

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

すべてのプロダクション・コードは以下の2つの視点で分類できる。

  • コードの複雑さ
  • もしくはドメインにおける重要性

協力者オブジェクトの数コードの複雑さは分岐の数で計測でき、その数が多いほど複雑さが増す。

ドメインにおける重要性とは、そのコードがエンドユーザにとってどのくらい重要なのかで判断できる。
単体テストを行う価値が最も高いコードは複雑なコード・もしくはドメインにおける重要性が高いコードである。
理由は、退行に対する保護が備わるようになるためである。

協力者オブジェクトの数が多ければ多いほどテストコードの量が増えるため、保守のしやすさは低下する。

以上の2つの観点で見ることで、コードは4種類に分類できる。

  • ドメイン・モデル/アルゴリズム(重要性高、協力者オブジェクト低)
  • 取るに足らないコード(重要性低、協力者オブジェクト低)
  • コントローラ(重要性低、協力者オブジェクト高)
  • 過度に複雑なコード(重要性高、協力者オブジェクト高)ドメイン・モデル/アルゴリズムはテストをした場合の効果が最も高くなる。

取るに足らないコードはテストするのに見合った効果がないためテストしないほうがよい。
コントローラーは、包括的な統合テストの際にその中の一部として検証するのがよい。

過度に複雑なコードに関しては、テストしなければならないものの、テストすることが困難である。
そのため、ドメイン・モデル/アルゴリズムとコントローラーに分割し、テストを行う必要がある。

過度に複雑なコードを分割する場合、質素なオブジェクトと呼ばれる設計パターンの導入が考えられる。
まず過度に複雑なコードからテストしやすい部分を抽出し、その部分を包み込む質素なクラスを作成する。
その質素なクラスにロジックを含ませないようにすることでテストする必要がないようにする。
そこで新たに作成されるクラスが質素なオブジェクトである。

この質素なオブジェクトを作成するためのアーキテクチャとして、ヘキサゴナル・アーキテクチャと関数型アーキテクチャがある。
質素なオブジェクトを取り入れることで、単一責任の法則を守ることにもつながる。

ビジネス・ロジックに関する責務および連携の指揮に関する責務は、それぞれコードの深さおよびコードの広さとしてみることが出来る。
コードに対してどちらか一方の性質を持たせることはできるが、両方の性質を持たせることはしてはいけない。

7.2 単体テストに価値を持たせるためのリファクタリング

7.3 プロダクション・コードの種類に基づく効果的な単体テストの作成

→コードを使った実例のため割愛

7.4 コントローラにおける条件付きロジックの扱い

ビジネスロジックを分離しやすいのは、ビジネスオペレーションが以下の3段階になっている場合である。

  • ストレージからのデータの取得
  • ビジネス・ロジックの実行
  • 変更されたデータの保存

しかし、決定を下す過程の中で途中で得た結果を使ってプロセス外依存を呼び出すなどのこともあるため、必ずしも手順を遵守できない場合もある。

ビジネスロジックを分離する場合、以下の3つの性質についてバランスを考える必要がある。

  • ドメイン・モデルのテストのしやすさ
    →協力者オブジェクトの数と種類に影響される。

  • コントローラの簡潔さ
    →コントローラの決定を下す箇所の数に影響される。

  • パフォーマンスの高さ
    →プロセス外依存への呼び出しを行う回数に影響される。しかし、3つ全てを備えることはできず、2つ までしか備えられない。

ほとんどの場合、パフォーマンスが最重要となるため、「コントローラの簡潔さ」または「ドメイン・モデルのテストのしやすさ」のどちらかを選択することとなる。
しかし、「ドメイン・モデルのテストのしやすさ」を捨てた場合、テストのしやすさが失われる。

そのため、結果として「ドメイン・モデルのテストのしやすさ」、「パフォーマンスの高さ」を選択することとなる。

また、そもそもコントローラからすべての複雑さを取り除くことはできないため、管理可能なレベルまで抑えることが求められる。

次の2つの設計パターンにより、コントローラが複雑となることへ対策ができる。

  • 確認後実行パターン
    →何かを実行するメソッドに対して、実行可能かを確認するメソッドを用意することで、何かを実行するメソッド実行時に事前条件が必ず満たされるようになる。
     確認するメソッドを作ることでコントローラが決定を下す責務を本質的に取り除くことが出来る。

  • ドメイン・イベント
    →ドメインモデルで変更を発生させたビジネス・オペレーション終了後に、プロセス外依存へ変更を知らせるようにすることをドメイン・イベントという。

7.5 結論

抽象化する対象をテストするよりも、抽象化された結果をテストすることで簡単になる。

第8章

8.1 統合テストとは?

単体テストの3つの性質を持っていないテストは統合テストに分類される。
統合テストでは、コントローラ(重要性低、協力者オブジェクト高)のテストを行う。
前述のテスト・ピラミッドのように、実行時間と負荷の少ないものに関しては単体テストで行い、できるだけ単体テストで行うようにする必要がある。

ハッピーパスとは、テストが正常終了するシナリオであり、異常ケースはシナリオがエラーで終わる場合のことである。
統合テストでは、全てのプロセス外依存とのやり取りを検証するようなハッピーパスを見つけ出し、
ハッピーパスが見つからない場合には全ての外部システムとのやり取りが検証されるようにする。

異常ケースでシステムがすぐに停止するのであれば、テストする必要はなく、早期失敗させる選択肢もある。

8.2 どのようなプロセス外依存をモックに置き換えるべきか?

すべてのプロセス外依存は以下の2つに分類できる。

  • 管理下にある依存
    →テスト対象のアプリケーションが好きなようにできるプロセス外依存。
     外部から見ることはできない。

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

プロセス外依存の中には管理下にある依存と管理下にない依存の性質を併せ持つ依存もある。
そのような場合には、外部から観察可能な部分をモックに置き換え、観察できない部分は依存とのコミュニケーションではなく処理が終わった後の依存の状態にする。

もし統合テストにおいてDBが使えない場合は、統合テストではなくドメイン・モデルの単体テストにてテストを行うのがよい。

8.3 どのように統合テストを行うのか?

統合テストでは、管理下にある依存を扱う層をすべて経由して検証する必要がある。
DBのような場合は、入力とは別に確認フェーズで再度DBにアクセスして状態を確認する必要がある。

8.4 インターフェースを使った依存の抽象化

実装クラスを1つしか持たないようなインターフェースは、すべきではない(YAGNI原則から外れる)
理由としては、コード量は少なければ少ないほど良く、
ロジックを使う機会が訪れない場合は無駄なコストとなってしまうためである。

実装クラスを1つしか持たないインターフェースが妥当なのは、管理下にない依存に対する場合のみであり、管理下にある依存の場合は直接クラスを定義するようにする。
管理下にある依存に対して、実装クラスを1つしか持たないインターフェースが使われている場合は、
実装の詳細をテストケースに深く結びつけてしまうことにつながってしまう。

8.5 統合テストのベストプラクティス

ドメイン・モデルとは、アプリケーションで解決しようとしている問題のドメイン知識を集めたものである。
ドメイン・モデルはコード上の見つけやすいところに配置されるようにする。
ドメイン・クラスとコントローラの境界が明確になっていれば単体テストと統合テストを分離しやすくなる。

アプリケーションを構成する層を減らすことで、コードに対する理解がしやすくなる。
間接参照が多い場合、ドメインモデルとコントローラの境界線があいまいになり、リファクタリングへの耐性を失うことにもなる。
バックエンドのシステムであれば、ほとんどの場合ドメイン層、アプリケーション・サービス層、インフラ層の3つの層のみで十分である。

循環依存も、どこからコードを読めばいいかわからなくなるため、コードの理解をしにくくさせる要因となる。
循環依存は値オブジェクト(アプリケーション内のバリデーションのようなもの)を利用することで防ぐことが出来る。

1つのテストケース中に実行フェーズを複数持つことが妥当なのは、プロセス外依存に何らかの制限がある場合である。
単体テストではプロセス外依存がないため、基本的に2回実行フェーズを記載してはいけない。

8.6 ログ出力に対するテスト

サポートログは、サポートスタッフやシステム管理者によって見られるため、観察可能な振る舞いとなる。
診断ログは、開発者がアプリケーションの中で何が起こっているのかを判断するためのログであり、
実装の詳細に分類される。

サポート・ログは観察可能な振る舞いであるため、他のプロセス外依存を扱う機能と同じように扱う必要がある。
ドメイン・モデルで状態の変化を追跡する場合、ドメインイベントとして扱う必要がある。

診断ログに関しては、テストする必要はなく、ドメイン・モデルで直接出力しても問題はない。診断ログは過度に使ってしまうとノイズとなるため、基本的に想定外のことが起こった場合のみ使うようにする。

8.7 結論

全ての振る舞いについて、観察可能な振る舞いなのか実装の詳細なのかを判断する必要がある。
ログ出力に関しても同じことがいえる。

第9章

9.1 モックの価値を最大限に引き出す方法

管理下にない依存とのコミュニケーションを検証する場合、コントローラからその依存に向かう流れの最後のコンポーネントとなるものを、モックの置き換え対象にする。
それによって、退行に対する保護に関しては、統合テストによって多くのコードが検証されるようなり、
リファクタリングへの耐性に関しては、実装の詳細からモックを切り離すことが出来る。

スパイはテスト・ダブルの一種で、モックはフレームワークによって作られるのに対し、スパイは開発者の手で作られる。
テスト・ダブルの対象がアプリケーションの境界にいる場合、モックよりもスパイの方が優れていると考えられる。

確認の際は、プロダクション・コードを信用せず、テストではコードに定義されたリテラルや定数を使わないようにする。
コードに定義されているリテラルや定数を使った場合、同じことを検証することとなってしまい、
同義語反復のテストケースが作成されることになってしまう。

9.2 モックのベストプラクティス

モックを使うのは、統合テストのみにして単体テストでは使わない方がよい。
単体テストにて検証されるのはドメイン・モデルのみであり、プロセス外依存が存在しない。

統合テストにて検証されるコントローラにおいてはプロセス外依存が存在するため、モックを使用のは統合テストのみとなる。

1単位の振る舞いを検証することとモックの数の間には何の関係もなく、必要に応じて複数回モックを使用してもよい。

モックを用いたテストでは、想定されている呼び出しが行われていること・想定していない呼び出しが行われていないことの、両方を確認しなくてはならない。

モックの対象になる型は、自身のプロジェクトが所有するコードベースの型だけに制限しなくてはならない。
モックを作成する際はライブラリを直接置き換えるのではなく、ライブラリに対するアダプタを作り、
そのアダプタに対してモックを作成するのがよい。

第10章

10.1 データベースをテストするのに必要な事前準備

データベースのスキーマ情報をGit等で管理する方法は、以下の懸念があるため避けた方が良い。

  • 変更の履歴を辿れなくなる
    →スキーマが過去にどうなっていたのかわからなくなり、本番環境のバグを再現できない。

  • 真実が1つではなくなる
    →ソースコード上にて、どのスキーマが正であるかを整理する手間が出るため、余計な負担となる。

スキーマを管理する場合、テーブルやビューなどと共に必要な参照データ(=テストデータ)をINSERT文などで用意しておくと、同じ環境が再現しやすい。

1つのデータベース・インスタンスを複数の開発者で共有してテストする場合、効率が悪いため1人に1つずつデータベース・インスタンスを用意するとテストしやすい。

データベースの本番への反映方法には以下の2種類がある。

  • 状態ベース
    →データベースがどのような状態であるか(スキーマの内容)をソースコードで管理。

  • 移行ベース
    →データベースに何をしたか(DDL等)をソースコードで管理。

基本的には移行ベースの方が優れている。

理由としては、例えばカラムを分割しようとした際、状態ベースではカラムの分割を記録することはできるが、データに対してどのような影響を与えたかについては記録できないためである。

ただ、まだリリースされていないサービスの場合、本番データが存在しないため、状態ベースでのテストでも有効となる。

10.2 データベース・トランザクションの管理

トランザクションの利用においては、可能な限り単位作業に置き換えて実装する。

単位作業とは、1つのビジネスオペレーションの中でデータの変更が発生するオブジェクトを保持し、

全て完了した段階でDBを更新するパターンのことである。

単位作業の方が良い理由は、データベースに対する更新を後回しにすることができるためである。

その場合、DBに対してトランザクションが制御する時間を短くすることができる。

統合テストの場合はテストケースの異なるフェーズ(準備、実行、確認)で、同じトランザクションや単位作業を使いまわさないようにすることが重要である。

10.3 テスト・データのライフサイクル

統合テストでは、テストケースを同時に実施するメリットは少なく、1つずつ実行する方が現実的である。

統合テスト実施にあたり、それぞれのテストケースで扱うデータが干渉してしまう場合がある。

そのため、テストケース実行前にデータの後始末を行うことが望ましい。

後始末については、外部キー制約等を考慮した上でSQL実行にて行うのが良い。

テストの場合は、インメモリデータベースを使わないようにする。
テストはできるだけ本番環境に近い形で行う必要がある。

10.4 テストコードの再利用

テストケースはそれぞれが独立しており、依存しない前提でなくてはならない。

テストコードの量を減らすには、ビジネスロジックに関わらない技術的なコードを、ヘルパーやプライベート関数で分離しておくのが良い。

第11章

11.1 プライベートなメソッドに対する単体テスト

プライベートなメソッドに対しては、テストすべきではない。
プライベートなメソッドに対してテストすることは、「観察可能な振る舞いのみを検証する」ことに反する。

プライベートなメソッドをテストしたいような場合であれば、観察可能な振る舞いの一部に含めることで、間接的に検証させるようにする。

また、プライベートなメソッドを観察可能な振る舞いの一部としてテストした場合でも、完全に網羅できない場合がある。

観察可能な振る舞いの網羅はできていた場合、以下の2つの問題があると考えられる。

  1. デッドコード
    →リファクタリングをした際に、実際に使われないコードが残ってしまう場合がある。
     そのようなコードは削除する方がよい。

  2. 抽象化の欠落
    →プライベートなメソッドが複雑すぎて、十分に検証できないのであれば、抽象化ができていない可能性が考えられる。
    そのため、抽象化できる部分を別のクラスとして抽出した方がいい。

11.2プライベートな状態の公開

テストのためにプライベートなAPIを公開してはいけない。

テストでは、本番環境と全く同じ状況で行う必要があるためである。

テストのために、何か特別なことをするのは許されない。

11.3テストへのドメイン知識の漏洩

テスト作成の際、プロダクション・コードに定義された、特定のロジックやアルゴリズムをテストコードに持ってきてはいけない。

テストコード内では、プロダクション・コードのロジックを使って期待値を出すのではなく、期待値に直接値を書き込んでテストを行うようにする。

11.4プロダクション・コードへの汚染

プロダクション・コードが、テストの場合のみ振る舞いを変える場合、テストでのみ必要とされるコードがプロダクション・コードに記載されてしまうことがある。

インターフェースを導入し、プロダクション・コードとテストコードを分けることで、インターフェースで定義したメソッドに関しては、テストコードで用いるロジックが入り込む可能性がなくなる。

インターフェースを導入すること自体は、プロダクション・コードへの汚染に変わりないが、プロダクション・コードに、テストコードのロジックが入ってしまうことを防ぐことが出来る。

11.5具象クラスに対するテスト・ダブル

既存の機能を使うために、具象クラスをテスト・ダブルに置き換えるのはアンチパターンとなる。

置き換える必要があるような場合は、具象クラスが単一責任の原則を守れていない場合が考えられる。

11.6単体テストにおける現在日時の扱い

現在日時を環境コンテキストとして表現することは、プロダクション・コードの汚染につながるため、テストの実施が難しくなる。

そのため、現在日時は明示的に依存として注入する必要があるが、可能な限りサービスではなく値を注入するのがよい。

11.7結論

あとがきのようなもののため、割愛します。

Discussion