Zenn
Closed13

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

ShimmyShimmy

1章

学んだ内容

  • テストコードもプロダクションコードと同じように「資産でなく、負債」
  • 単体テストが作成しづらい -> 設計の質が悪い(しかし、逆は成立しない)
  • 単体テストをしようとすることで依存の多さなど設計の問題が明確になるのは良いこと。
  • テストカバレッジが低い -> テストスイートの質が悪い(しかし、逆は成立しない)
  • 単体テストを作ることは重要だが、それ以上に良い単体テストを作成することが重要
    • つまり、問題領域において意味があるものを検証する
  • 優れたテストスイートの特徴
    1. テストすることが開発サイクルの中に組み込まれている
    2. コードベースの特の重要な部分のみがテスト対象になっている
    3. 最小限の保守コストで最大限の価値を生み出す
ShimmyShimmy

2章

この章では隔離について、古典学派とロンドン学派の考えが書かれている。本書では古典学派の考えが軸となっており、そちらが中心で話が進むのでロンドン学派については詳しく書かない

学んだ内容

  • 単体テストの定義は次の性質をすべて持つもの
    1. 1単位の振る舞いを検証する
    2. 実行時間が短い
    3. 他のテストケースから隔離された状態で実行される
  • 隔離についての考え方
    • 古典学派: 単体のテストケースを別のテストケースから隔離。単体とは1単位の振る舞いのこと
    • ロンドン学派: テスト対象システムを協力者オブジェクトから隔離
  • Integrationテストとは、単体テストが持つべき3つの性質を1つでも欠いたテストのことである

依存について

種類 内容
共有依存 テストケース間で共有される依存。これがあるとテストを並列実行できない
プライベート依存 共有されない依存
可変依存 プライベート依存のうちイミュータブルな依存
不変依存 プライベート依存のうちミュータブルな依存
プロセス外依存 アプリケーションを実行するプロセスの外で稼働する依存
  • プロセス外依存は共有依存である場合が多いが、常にそうとは限らない。
    • 例えばDBはプロセス外依存 && 共有依存である。
    • しかしDBを異なるDockerコンテナ上で実行させて、そのコンテナ内にDBを用意した場合プロセス外依存になるが、共有依存ではない。
  • 古典学派がテストダブルに置き換えるのはテストケース間で共有される共有依存である。

その他

Unitテストがどのようなテストか、Integrationテストがどのようなテストか。などのテストサイズにおいては, Googleのテストサイズの考えを用いると良さげ

ShimmyShimmy

3章

AAAパターン

  • 単体テストのテストケースが準備(Arrange), 実行(Act), 確認(Assert)の3つのフェーズで構成されていること
    • テストケースに同じフェーズが複数含まれる場合、そのテストケースは複数の振る舞いを一度に検証していることを示唆している
      • Actが1行でないといけない。ということではない
    • 実行フェーズに記述するコードが1行を超すのであれば、テスト対象となるコードのAPIがきちんと設計されていないことを示唆している

テストメソッド名のつけかた

  • テストメソッド名は問題領域に精通している非開発者にも伝わる事実を付けるようにする
    • ドメインエキスパートに伝わるよう、「振る舞い」を簡潔に表す
    • 非開発者にも伝わるという箇所については私は疑問
  • 単体テストですべきことは、プロダクションコードが何をするのかを列挙することではなく
    アプリケーションの振る舞いについて、高いレベルで描写すること
  • テストメソッド名は厳格な命名規則を使わない
    • 単体テストのよくある名付け方であった{テスト対象メソッド}_{事前条件}_{想定する結果}
      みたいなのは実用性がない
  • テストメソッド名にはテスト対象のメソッド名を含めない
  • ライブラリなどを利用して、何をテストしているのかなどを読みやすくする
ShimmyShimmy

古典学派とロンドン学派の比較が結構長いな
これだけ長いと、著者が古典学派推しなら、古典学派の話だけで良いじゃんとも思う

ShimmyShimmy

4章

テストケースを作成する際は

  • 作成するテストケースが、問題領域に関する物語を伝えているか。ということを意識する
  • ブラックボックステストを用いるようにする

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

テストケースの価値は次の4本の柱の掛け算で決まる

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

1. 退行(regression)に対する保護

  1. テスト時に実行されるプロダクションコードの量
  2. そのコードの複雑さ
  3. そのコードが扱っているドメインの重要性

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

偽陽性(false positive)を生み出すことなく、プロダクションコードに対してリファクタリングを行えるのかを示す性質。テストケースがテスト対象の内部的な実装と結びつくことでリファクタリングへの耐性がなくなる。

1.2のまとめ

退行に対する保護とリファクタリングへの耐性の2本の柱はテストの正確性を最大限に引き出す

  • regressionに対する保護-> false negativeからプロダクトを守る
  • リファクタリングへの耐性 -> false positiveの数を最小限に抑える

4. 保守のしやすさ

次の2つのことから評価できる

  • 何をテストしているのかを簡単に理解できるか
  • テストを実施することがどのくらい難しいか

トレードオフ

これら4つの柱をすべて最大限の備えることは不可能
最善の単体テストは、まずは「保守のしやすさ」と、「リファクタリングへの耐性」を最大限に備える必要がある。その上で
「退行に対する保護」と「迅速なフィードバック」
がトレードオフになりどちらを優先するかのバランスが必要になってくる

ShimmyShimmy

5章

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

  • テストダブルとは、テストの際に使われる偽りの依存として表現されるものを包括的に意味する
    • ただ実質的にはモックという単語が一般的に使われていると思う

モックとスタブ

項目 内容 CQRS
モック 外部に向かうコミュニケーション(出力)を模倣・検証する場合に使う コマンド
スタブ 内部に向かうコミュニケーション(入力)を模倣するのに使う。
スタブのテストは行わない
クエリ

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

  • 「観察可能な振る舞い」は次の2つのどちらかになるコード
    • 操作: 計算をしたり、副作用を起こしたりすつメソッド
    • 状態: システムの現時点でのコンディション
  • どちらでもないものは「実装の詳細」
  • きちんと設計されたコードは
    • 観察可能な振る舞い => 公開されたAPI
    • 実装の詳細 => プライベートなAPIとして隠される
  • false positiveを減らすにはテストで検証する対象を「最終的な結果」にし、実装の詳細にはならないようにする

アーキテクチャ

  • ヘキサゴナル(各種)アーキテクチャにおいて、アプリケーションの各層は観察可能な振る舞いだけを公開し、実装の詳細を内部に隠すようになっている
  • これ徐々にファイル -> API -> アーキテクチャとフラクタルになっているな
コミュニケーションの種類 対象 振る舞い / 詳細 モックの可否
システム内コミュニケーション クラス間でのコミュニケーション 実装の詳細 使ってはいけない
システム間コミュニケーション 外部アプリケーション 観察可能な振る舞い 使って良い
ShimmyShimmy

6章

単体テストの3つの手法
関数型について

出力値ベーステスト

  • 戻り値を確認するテスト
  • 最も質の高いテストケースを作成できる
  • 前提
    • テスト対象のコードには隠れた入力や出力がなく、戻り値だけが唯一の実行結果となっている
    • 処理を行った後もテスト対象システムの状態と協力者オブジェクトの状態が変わらない(副作用がない)
  • すべてのテストを出力値ベーステストにするのではなく、できるだけ多くのテストを出力値ベーステストにすることを目指す

状態ベーステスト

  • 状態を確認するテスト
  • 状態とは
    • テスト対象システムの状態
    • 協力者オブジェクトの状態
    • プロセス外依存の状態
  • テストが壊れやすくならないように注意
    • プライベートな状態は公開しない
    • ヘルパーメソッドや値オブジェクトで保守性を改善する

コミュニケーションベーステスト

  • オブジェクト間のやり取りを確認するテスト
  • モックを用いてテスト対象と協力者オブジェクトの間で行われるコミュニケーションをテストする
  • 3つの手法の中で最も保守が難しい
  • コミュニケーションベーステストを用いても良い条件
    1. 検証の対象となるコミュニケーションがアプリケーションの境界を超えて行われる
    2. 外部から確認できる副作用を発生させる

関数型プログラミング

数学的関数を用いたプログラミングのこと
数学的関数とは、隠れた入力や出力を持たない関数

内容
隠れた入力 内部の状態や外部の状態を参照すること
隠れた出力 副作用や例外

関数型を導入することで

  • 関数型プログラミングでビジネスロジックと副作用を分離したい
  • 関数型アーキテクチャで副作用をビジネスオペレーションの最初や最後にもっていくことで、分離を実現する。
    • 関数型で書かれたコードの割合を最大に
    • 副作用を扱うコードを最小限に

関数型アーキテクチャにおけるコードの分類

内容
関数的核(不変核) 決定を下すコード
可変核 決定に基づくアクションを実行するコード。
関数的核で下された決定に基づいて副作用を起こす

可変核と関数的核の画像。卵の殻と中身のように、可変核が関数的核を包んでいる2重構造になっている。可変核から関数的核に対して、入力データが与えられ、関数的核から可変核に対して、決定という出力が与えられている。

関数型アーキテクチャとヘキサゴナルアーキテクチャの違い

  • 関数型アーキテクチャ: すべての副作用がドメインの外に出される
  • ヘキサゴナルアーキテクチャ: ドメイン層で副作用を起こしても、副作用がそのドメイン層内に制限されている限り問題はない

参考資料(動画)

ShimmyShimmy

7章

単体テストの価値を高めるリファクタリング

  • 複雑なコード、およびドメインにおける重要性があるコードはテスト対象として価値あるもの
  • テストの対象となるコードが持つ協力者オブジェクトが増えれば増えるほど、そのテストの補修コス地は高くなる

プロダクションコードの4種類の分類

横軸が協力者オブジェクトの数、縦軸がコードの複雑さ/ドメインにおける重要性

分類 内容
ドメインモデル/ アルゴリズム ここのテストに対する費用対効果が最も高い
取るに足らないコード テストする価値がまったくない
コントローラー 単体テストではなく統合テストでテストされるべき
過度に複雑なコード コントローラーとドメインモデル/アルゴリズムに分けるべき。
Fat Controllerなどがあてはまる

テストしやすくするために

コードに対する複雑さや重要さが増すに連れ、協力者オブジェクトの数を減らすようにすべき

質素なオブジェクト(Humble Object)

  • 過度に複雑なコードからビジネスロジックを別のクラスに抽出することでテストを行いやすくする設計パターン。あとに残ったコードがHumble Object -> Controller
    • Contoller: 連携を指揮することに注力
    • ドメインモデル: コードの深さや重要性に注力
  • ヘキサゴナルアーキテクチャや関数型アーキテクチャはHumble Objectによる設計パターンを採用したアーキテクチャ
    • ヘキサゴナルアーキテクチャ: ビジネスロジックをプロセス外依存とのコミュニケーションから隔離
    • 関数型アーキテクチャ: ビジネスロジックをすべての協力者オブジェクトとのコミュニケーションから隔離

ビジネスロジックと、連携を指揮するコードの分離

分離の際にバランスを考えなくてはいけない3つの重要な性質。

  1. ドメインモデルのテストのしやすさ
    協力者オブジェクトの数と種類が少ないほうが良い
  2. Controllerの簡潔さ
    Controllerの決定を下す箇所(分岐)の数が少ないほうが良い
  3. パフォーマスの高さ
    プロセス外依存の呼び出しを行う回数が少ないほうが良い

これら3つの性質は2つまでしか備えることができない

手法とトレードオフ

  • 外部依存に対する読み書きをビジネスオペレーションのはじめと終わりにする
    • Controllerとドメインモデルのテストしやすさは向上する。しかしパフォーマンスが犠牲になる
  • ドメインモデルにプロセス外依存を注入
    • パフォーマンスとコントローラーの簡潔さは維持できる。しかしドメインモデルのテストが難しくなる
  • 決定を下す過程をさらに分割する
    • ドメインモデルのテストしやすさとパフォーマンスは維持できる。しかしコントローラーが複雑になる

決定を下す過程をさらに分割する

コントローラーが複雑になることの対策方法

  • 確認後実行(CanExecute/Execute)パターン
    • 何かを実行するメソッドに対して、それが実行可能かを確認するメソッドを用意することで、処理が正しく実行されるための事前条件が必ず満たされることを保証する設計パターン
    • ビジネスにおける決定をドメイン層に集められる
  • ドメインイベント
    • ドメインモデルで発生する重要な状態の変更を追跡する。発生したドメインイベントを元にプロセス外依存への呼び出しを行うようにする
    • ドメインイベントとは、「ドメインエキスパートにとって意味のある」アプリケーションのイベントを表現するもの。

抽象化する対象をテストするより、抽象化された結果をテストするほうが簡単。

  • ドメインイベントはプロセス外依存への呼び出しに対する抽象化
  • ドメインクラスに対する状態の変更はデータストレージへの状態の変更に対する抽象化

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

  • 観察可能な振る舞いと実装の詳細との関係は玉ねぎの層のように考えられる。各層のテストは1つ上の視点で行い、テスト対象となる層がさらに下の層と何をしているかは意識しない
  • 先ほどまでは実装の詳細だったものが、視点が変わることで観察可能なふるまいとなり、その視点でのテストを新たに実施することになる
ShimmyShimmy

8章

なぜ、統合(Integration)テストを行うのか?

統合テストとは

統合テストでは、テスト対象システムがプロセス外依存と統合した状態でどのように機能するのかを検証する

  • 統合テストはコントローラーに分類されるコードを検証する
  • 単体テストではビジネスシナリオのおける異常ケースを可能な限り多く扱うようにする一方、統合でストでは1件のハッピーパス、および単体テストでは扱えなかったテストケースを可能な限り多く扱う
    • すべての外部システムとのやり取りを検証できるようテストケースを増やす

管理下にある依存と管理下にない依存

プロセス外依存は管理下にある依存と管理下にない依存の2つに分けられる

分類 これとのコミュニケーションは 詳細 可視性 テストにおいて
管理下にある依存 実装の詳細 テスト対象のアプリケーションを経由することでしたアクセスできないプロセス外依存 コミュニケーションは外部から見ることはできない テスト対象のアプリケーションしかアクセスしないDB 実際の依存(インスタンス)を使う
管理下にない依存 観察可能な振る舞い 他のアプリケーションからもアクセスされるプロセス外依存 コミュニケーションは外部から見ることができる メールサービス
メッセージバス
モックを使う

管理下にある依存と管理下にない依存の両方の性質を持つもの

  • 例: 他のアプリケーションからもアクセスされるテーブルをいくつか持っているデータベース
  • 外部から観察可能な部分
    • 管理下にない依存として扱う
    • 依存をモックに置き換える
  • 外部から観察できない部分
    • 管理下にある依存として扱う
    • その依存とのコミュニケーションではなく、処理が終わった後の依存の状態を検証する

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

  1. ドメインモデルの境界を明確にする
  2. アプリケーションを構成する要素を減らす
  3. 循環依存を取り除く

その他

  • 間接参照の層が過度に多くなるとコードの理解用意性が失われるので、間接参照の層はできるだけ少なくなるようにすべき
    • バックエンドシステムならドメイン層、アプリケーション層、インフラ層の3層で十分
  • 実装クラスを1つしか持たないようなインターフェイスが同一プロセス内の依存に使われているのであれば、その設計には何か問題を抱えている可能性が高い
    • モックを使ってドメインクラス間でのやり取りを検証している。ことを示唆している
管理下にある依存 管理下にない依存
実装クラスが1つだけ 具象クラス インターフェイス
実装クラスが複数 インターフェイス インターフェイス
ShimmyShimmy

9章

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

  • 管理下に無い依存とのコミュニケーションを検証する時にコントローラーから、その依存に向かう流れの中で、最後のコンポーネントとなるものをモックの置き換え対象にする
  • スパイ: 「手書きのモック」と呼ばれるもの。システムの境界にあるクラスをモックするなら、スパイのほうが優れている
  • 確認の際はプロダクションコードを信頼してはならない。テストではプロダクションコードに定義されたリテラルや定数を使わない
  • モックを使うのはコントローラーを検証するとき、つなり統合テストを行うときだけ
    • モックは管理下にない依存にのみ利用するから
    • 管理下に無い依存を扱うのはコントローラーだけであるから
  • モックを用いたテストでは以下の両方を確認する
    • 想定している呼び出しが行われていること
    • 想定していない呼び出しが行われていないこと
    • モックの対象になる型は自身のプロジェクトが所有するコードベースの型だけに制限する
      • 管理下に無い依存へのアクセスにサードパーティライブラリを使用しているのであれば、そのライブラリを内包する独自のアダプタを作成し、自身で作成したアダプタに対してモックを作るようにする
ShimmyShimmy

10章

データベースに対するテスト

  • 可能な限り、作業単位(unit of work)パターンを採用する
  • テストケースの異なるフェーズで同じデータベーズ取っランザクションを使わない
    • Arange, Act, Assertではそれぞれ個別のデータベーストランザクションを持つようにする
  • テストケースの実行が始まった時に、他のテストケースが残したデータの後始末を行う
  • テストの際にin memoryデータベースを使わないようにする
  • 読み込みに対するテストは書き込みに対するテストより重要性は低い
    • 読み込みをテストするのであれば複雑な読み込み or 重要な読み込みだけをテストする
  • リポジトリを直接テストするのではなく、そのリポジトリを統合テストのシナリオの一部に含ませることで間接的に検証されるようにする
ShimmyShimmy

11章

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

  • 単体テストを行うためにプライベートメソッドを公開しない
    • プライベートメソッドを直接テストするのではなく、そのプライベートメソッドを観察可能な振る舞いの一部に含めて間接的にそのメソッドを検証する
  • プライベートメソッドの検証を観察可能な振る舞いの一部としてテストすることがあまりにも難しいのであれば、テスト対象のコードで抽象化の欠落が起こっている可能性が高い
    • テスト対象のコードに対して適切な抽象化を行い、その抽象化したものを別のクラスとして抽出し、抽出したクラスに対して検証を行う
      • テストコードからプロダクトコードに示唆が与えられることが大事

テストでは品質は上がらないですよ。テストはあくまでも品質をあげるきっかけ。品質をあげるのはプログラミングです。これは大昔からそう。
http://kakutani.com/20090213.html#p03

  • テストを作成する際、プロダクトコードのアルゴリズムやロジックの知識をそのままテストに持ってきてはならない
    • ブラックボックステストの観点でテストする
  • 現在日時は明示的に依存として注入させなくてはならない
    • サービスとして注入する
    • 値として注入する
      • 可能な限りこちらを選択する
このスクラップは3ヶ月前にクローズされました
作成者以外のコメントは許可されていません