Closed69
[読書]単体テストの考え方/使い方
第1部 単体テストとは
第1章 なぜ、単体(unit)テストを行うのか
- 単体テストのベストプラクティスを実施すれば、設計はよりよいのものになる
- 単体テストで達成したいことは、持続可能な開発
- 質の悪いテストコードは価値がない、マイナスにもなりうる。
- コードは資産ではなく負債。コードが多いほどバグが増える
網羅率とテストスイートの質との関係
- 網羅率を使ってもテストスイートの質を評価することはできない。
- 実際にコードの検証がされたのかを保証できないため。
- 網羅率は検証されたことを示すのではなく、実行されたことを示す。
- 検証されず実行されたことだけを見ているテストを確認不在のテストと言う
- 使用するライブラリ内のコードは計測対象外となるため
- 実際にコードの検証がされたのかを保証できないため。
- 網羅率の数値を義務化にすると、開発の妨げになり、適切な単体テストを行いづらくする状況を作り出してしまう。
- 網羅率はテストコードの質が悪いことは判断できる。良いことは判断できない
- 優れたテストスイートの特徴
- テストすることが開発サイクルに組み込まれている
- コードベースの特に重要な部分のみテスト対象としている
- ドメインモデルを対象としたコードが重要な部分となる。
- 最小限の保守コストで最大限の価値を生み出すようにしている
- そのために必要なこと
- 価値のあるテストケースを認識できること
- 価値のあるテストケースを作成できること
- そのために必要なこと
- 単体テストの定義
- 単体と呼ばれる少量のコードを検証する
- 実行時間が短い
- 隔離された状態で実行される
- 古典学派:テスト・ダブルを使わない。
- ロンドン学派:テスト・ダブルを使う。
- テスト対象メソッド
- テストの際に呼び出されるテスト対象システムのメソッド
- テスト・ダブル:テストで使われるすべての種類の偽りの依存のことであり、プロダクトで使われることはない。
- モック:このような偽りの依存の一種でしかない。
古典学派が考える隔離
- 単体テストにおいて隔離する必要があるのはテストケースである。
- 共有依存:テストケース感で共有される依存。e.g.) static 変数
- 共有依存は単体テストのテストケース間で共有される依存のこと。
- シングルトンオブジェトであってもテストケース毎に新しいインスタンスを作成するようにしている限り共有依存にはならない。
- プロセス外依存:DB やファイルシステムに依存すること。
- 不変依存:不変オブジェクトに依存している。e.g.) 値オブジェクト。値。
- 不変依存にはテストダブルは使わない。
- 共有依存であってもプロセス外依存ではないもの(e.g.シングルトン)は滅多にない
- テストケースごとに依存インスタンスを作成すれば共有を防ぐことができる
- 共有されないプロセス外依存を扱うことも滅多にない
- プロセス外依存は可変であるため、単体テストを実行すれば 状態が変わる。
- ロンドン学派の単体テストの長所
- より細かな粒度で検証ができる
- 依存関係が複雑になっていても簡単にテストすることができる
- テストが失敗した際、どの機能に問題があったのかを正確に見つけられるようになる
本当に長所なのか深堀り
- 細かな粒度による検証
- 単体テストにおける単体とは、1単位のコードではなく、1単位の振る舞いを指す。
- そのため、1つのふるまいを表すためにいくつかのクラスを跨ぐこともありうる。
- コードの粒度を細かくしすぎると、何を検証するテストなのかが分からず、質の悪いテストになってしまう。
- 単体テストにおける単体とは、1単位のコードではなく、1単位の振る舞いを指す。
- 複雑な依存関係を持つものに対する単体テスト
- 複雑な依存関係を持つテストはテストダブルを用いることがはるかに簡単になる。
- しかし、問題なのは複雑な依存を持っているシステムそのもの。
- テストが失敗した際、どこに問題があったか分かる
- テストコードが正確に分かる。
- 古典学派のスタイルだったとしても頻繁に単体テストをしていれば問題点は分かる。
- また、1つのコード修正で多くのテストが失敗する場合多くのクラスに依存されていたということが分かるメリットもある。
- テストコードが正確に分かる。
- その他の古典学派とロンドン学派の違い
- テスト駆動開発を用いたシステム設計
- ロンドン学派:モックを使えば1つのクラスにのみ実装を専念できる。外側から内側に向かうテスト駆動開発。
- 古典学派:実際のプロダクションコードを使うので、実装できたタイミングで繰り返しテストを追加する。内側から外側に向かうテスト駆動開発。
- 筆者は古典学派より
- ロンドン学派のほうが実装の詳細に深く結びつく傾向がありそれに賛同できない。
- テスト駆動開発を用いたシステム設計
- 統合テストにおけるロンドン学派と古典学派の違い
- 古典学派における単体テストはロンドン学派における統合テストに分類される。
- 古典学派の単体テストの観点
- 1単位のふるまいを検証すること
- 実行時間が短いこと
- 他のテストケース空隔離された状態で実行されること
- E2Eテストと統合テストの違い
- 一般的に統合テストは1個か2個のプロセス外依存を扱う。
- E2Eテストはほぼ全てのプロセス外依存を扱う。
- エンドユーザの視点でシステム検証をするため。
- 統合テストでは内部のプロセス外依存はそのまま、外部のプロセス外依存はモックを使うという設計をする。
- 外部のプロセス外依存はこちらでは好きなように状態を変えることができないため。
- E2Eテストは非常にコストがかかるため、単体テスト、統合テストが完了してから行う。
第3章 単体テストの構造的解析
- AAA パターン:準備(Arrange)、実行(Act)、確認(Assert)で構成される単体テスト
- 完結で統一された構造をもたせられるようになる。
- 似たようなパターンとして、 Given-When-Then パターンがある
- 単体テストにおいて回避すべきこと
- 複数のふるまいを検証すること。
- それは統合テストにあたる
- if 文の使用
- 複数のふるまいを検証することを示唆している。
- テストが読みづらくなり理解しづらい。
- 複数のふるまいを検証すること。
- 各フェーズの適切なサイズ
- 準備フェーズが一番大きい
- 実行フェーズが1行より多い場合は要注意
- 適切なカプセル化ができているか確認する必要がある。
- 確認フェーズの項目は必ずしも1つである必要はない。複数あってよい。
- しかし、大きくなりすぎるのであれば適切な抽象化ができているか確認する。(オブジェクトを準備する等)
- 単体テストでは後始末のフェーズは不要。
- 単体テストでは副作用が起こらないため。起こるのであればそれは統合テストである。
- テスト対象システムの変数名は sut(System Under Test) と名付けるようにする。
- テストに置いてどのクラスを対象としているのかをわかりやすくするため。
- 各フェーズのコードか分かりやすくする。
- 各フェーズを空白行で区切る
- 各フェーズ内で空白行を使いたい場合はフェーズの区切りでフェーズを示すコメントを付ける
- 単体テストですべきことはコードが何をするのかを単に列挙することではなく、アプリケーションのふるまいについて高いレベルで描写すること
テストケース間で共有するテストフィクスチャ
- テスト・フィクスチャ:テストを実施する際に使われるオブジェクトのこと。
- 適切ではないテスト・フィクスチャ
- コンストラクタでテスト・フィクスチャを定義する。
- テストケース間の結びつきが強くなってしまう。
- 1つのテストケースに関する修正が他のテストケースに影響を与えてはいけない。
- テストケースが読みづらくなってしまう。
- 準備フェーズのロジックがテストメソッド内になくコンストラクタまで見に行く必要がある。
- テストケース間の結びつきが強くなってしまう。
- コンストラクタでテスト・フィクスチャを定義する。
- テストフィクスチャを共有するためのより良い方法
- プライベートなファクトリメソッドを導入する
- テストコードの量を減らせる
- 各テストケースで何が行われるか読みやすくなる
- ただし、準備フェーズが単純なものであれば、導入しないほうがわかりやすい
- コンストラクタで行ってもよいもの
- 全てのテストケースで共通の処理 (e.g. DB の datasource)
- プライベートなファクトリメソッドを導入する
単体テストでの名前の付け方
- テストメソッドに明確な名前をつけることは大事。
- 対象のコードがどのように振る舞うのかを把握できるようになるため。
- 非常に有名で役に立たない命名規則
- {テスト対象メソッド} _ {事前条件} _ {想定する結果}
- テスト対象メソッド:検証するメソッド名を記述
- 事前条件:どのような条件でそのメソッドをテストするのかを記述
- 想定する結果:そのメソッドを実行した際に想定する結果を記述
- このような命名規則は振る舞いではなく実装の詳細に目をつけている。
- {テスト対象メソッド} _ {事前条件} _ {想定する結果}
- 簡潔な普通の言い回しのほうが良い。
- 何を検証しているのか明確になり、厳格な命名規則の構造に縛られることもなくなる。
- 認知的負荷が減り、他の開発者や非開発者にも分かりやすい。
- テストメソッドに名前をつけるときの指針
- 厳格な命名規則に縛られないようにする
- 複雑な振る舞いを命名規則に従って表現するのは限界がある。
- 問題領域のことに精通している非開発者に対してどのような検証をするのかが伝わるような名前をつける
- ドメインエキスパートやビジネスアナリストを指す
- "_" を使って単語を区切るようにする
- 読みやすさ向上。
- テストクラスのクラス名は {クラス名}Tests とする。
- テスト対象のメソッド名がテストメソッド名に含まれない
- 単体テストはコードをテストするのではなく、振る舞いをテストしているため、メソッド名が何であるかは関係ない。
- 内容が変わらないがメソッド名が変わる場合、テスト名も変える必要が出てくる。
- これはコードとテストが結びついていることを示している。
- 例外として util クラスのテストはメソッド名を付けてもよい。
- util はビジネスロジックが含まれていないため。
- 厳格な命名規則に縛られないようにする
パラメータ化テストへのリファクタリング
- パラメータ化テスト:1単位の振る舞いが持つ各事実(ケース)を1つのグループとして1つのテストメソッドにまとめる
- テストフレームワークの機能を使うことで実現できる。
- パラメータ化テストを行うことで、テストコードの量は減らせるが、テストメソッドの検証内容が分かりづらくなる。
- 解決法として、正常系と異常系でテストグループを分ける。
- テスト対象が複雑になる場合は、無理にパラメータ化テストをせずそれぞれ分けた方が良い
確認フェーズの読みやすさの改善
- 確認を行いやすくするライブラリの導入
- 変更例
Assert.Equal(1, result);
-
result.Should().Be(1);
// 主語 動詞 目的語 になっている。
- 変更例
- デメリットがあるとすれば、不要なライブラリ依存が増えるということ。
第2部 単体テストとその価値
第4章 良い単体テストを構成する4本の柱
- 良い単体テストを構成する4本の柱
- 退行(regression)に対する保護
- リファクタリングへの耐性
- 迅速なフィードバック
- 保守のしやすさ
- 退行(regression)に対する保護
- テストをすることでバグの存在をいかに検出できるのかを示す性質。
- 退行とはバグのこと。
- コードは資産ではなく負債。
- 退行に対する保護がテストにどのくらい備わっているかを把握する指針
- テスト時に実行されるプロダクションコードの量
- そのコードの複雑さ
- そのコードが扱っているドメインの重要性
- 取るに足らないコードはテストをする価値がほとんどない。
- 退行に対する保護を最大限に備えるためには、テストの際にできるだけ多くのプロダクションコードを実行させる
- リファクタリングへの耐性
- テストが失敗することなく、どのくらいプロダクションコードのリファクタリングを行えるのかということ。
- 偽陽性(false positive):嘘の警告。テスト対象のコードが意図通りの振る舞いをしているにも関わらず、テストが失敗すること。
- 偽陽性が起きることによる影響
- テストの信頼性を損ない、テスト失敗が軽視され、本番環境にバグが入り込んでしまう。
- テストをセーフティネットとして見られなくなり、リファクタリングを敬遠してしまう。
- 偽陽性が起きることによる影響
- テストコードがテスト対象となるコードの詳細と深く結びつくと、リファクタリングへの耐性がなくなる
- 偽陽性を減らす唯一の方法は、テストコードをテスト対象の内部的なコードから切り離す。
- そのためにはテストコードが検証する対象を実行されたコードがもたらす最終結果にする必要がある。
退行に対する保護とリファクタリングへの耐性との関係
- 偽陰性:テストが成功したが、機能に欠陥がある。
- 退行に対する保護をすれば抑えられる。
- 偽陽性:テストが失敗したが、機能は正しい。
- リファクタリングへの耐性を上げれば抑えられる。
- プロジェクトの初期段階では偽陽性は大きな弊害にはならない。
- リファクタリングをする必要がなく、コード量も少ないため、影響があまりない。
- 迅速なフィードバック
- 速やかにテストを行えると、テストの頻度が増え、バグの検出が早くなる。
- 保守のしやすさ
- 2つの観点
- テストケースを理解することがどれくらい難しいのか。
-テストコードの量が少ないほど理解しやすくなる。- 単純な物理量を減らすということではない。
- テストを行うことがどれくらい難しいのか。
- プロセス外依存のための準備が必要かどうか。
- テストケースを理解することがどれくらい難しいのか。
- 2つの観点
理想的なテストの探求
- 良いテストは4つ性質の掛け算で決まる
- 退行(regression)に対する保護
- リファクタリングへの耐性
- 迅速なフィードバック
- 保守のしやすさ
- テストコードを含めた全てのコードは負債。
- 価値のないテストケースをいくつも用意するより、価値のあるテストケースを必要な分だけ揃えるほうが効果がある。
- 全てを満たしたテストケースを作成することはできない。
- 退行に対する保護、リファクタリングへの耐性、迅速なフィードバックは排反する性質であるため。
- 可能な限り満たすテストケースを心がける必要がある。
- 優先すべきは、リファクタリングへの耐性と保守のしやすさ
- リファクタリングへの耐性はテストケースに備えられるのか否かのどちらかしか選択できないため。
- テストスイートを堅牢にするためにもっとも優先すべきはテストの壊れやすさ(偽陽性)を取り除くこと
- 退行に対する保護と迅速なフィードバックは柔軟に対応できる。この2つの間でバランスを調整する。
- リファクタリングへの耐性はテストケースに備えられるのか否かのどちらかしか選択できないため。
テストの極端な例
- E2Eテスト
- 退行に対する保護、リファクタリングへの耐性は満たす。
- プロダクションコードを多く実行する、エンドユーザ視点から振る舞いだけを見ている。
- 迅速なフィードバックは満たしていない
- 実行時間が長い。
- 退行に対する保護、リファクタリングへの耐性は満たす。
- 取るに足らないテスト
- リファクタリングへの耐性、迅速なフィードバックは満たす
- 1行とかのコードのため。
- 退行に対する保護は満たさない。
- ほとんどバグることはない粒度のため。
- プロダクションコードと同じことを別の書き方で表現しているだけ。
- リファクタリングへの耐性、迅速なフィードバックは満たす
- 壊れやすいテスト(what ではなく how に目を向けたテスト)
- 退行に対する保護、迅速なフィードバックは満たす。
- リファクタリングへの耐性は満たさない。
- 振る舞いが同じでも実行の仕方が変わったら失敗とみなされるため。
ソフトウェアテストにおけるよく知られた概念
- テストピラミッド:テストスイートにおいて異なる種類のテストがそれぞれ特定の割合でテストケースを持つようになっているという概念
- 単位テスト、統合テスト、E2Eテスト。
- 単体テストほど数が多くエンドユーザーの観点が遠い。
- テストピラミッドの高い位置にあるそうであるほど退行に対する保護を備えなくてはならない。低い層のテストであれば迅速なフィードバックをより備えなくてはならない。
- ホワイトボックステスト
- 退行に対する保護に優れている
- リファクタリング耐性に劣っている
- ブラックボックステスト
- ホワイトボックステストの逆
- ブラックボックステストを優先させる必要がある。
- リファクタリングへの耐性を上げるため。
- しかし分析をする際はホワイトボックステストも有効(コード網羅率を計測するツールを用いて検証されていないケースを見つけテストを作成する場合)
- ブラックボックステストとホワイトボックステストを組み合わせることで、質を高められるようになる。
第5章 モックの利用とテストの壊れやすさ
- テストダブル:プロダクションコードには含まれず、テストでしか使われない偽りの依存として表現される全てものを包括的に意味する
- テストダブルに5つの種類があるが、大きくはモックとスタブの2つに分けることができる。
- モック:対象システムからその依存に向かって行われる外部に向かう出力を模倣する。
- メールの送信。(副作用が発生する)
- スタブ:依存からテスト対象システムに向かって行われる内部に向かう入力を模倣する。
- データの取得。(副作用は発生しない)
- モック:対象システムからその依存に向かって行われる外部に向かう出力を模倣する。
- テストダブルに5つの種類があるが、大きくはモックとスタブの2つに分けることができる。
- モックは状況によって異なる意味で使われることがある
- テストダブル = モックとして使われるケース
- モックライブラリから提供されるモックオブジェクトを作成するためのクラス
- Mock クラスは道具としてのモック
- Mockクラスのインスタンスがテストダブルとしてのモック
- 上記2つを混合しない。
- 道具としてのモックを使ってスタブを作成することも可能
- 上記2つを混合しない。
- モックは外部に向かうコミュニケーションを模倣したり、検証したりする。
- スタブとのやりとりは検証してはならない
- 最終的な結果を生み出すための一過程に過ぎないため
- スタブのコミュニケーションを検証するのはアンチパターン。テストが壊れやすくなる。(過剰検証)
- 最終的な結果を生み出すための一過程に過ぎないため
- モックとスタブの両方の性質を持ったテストダブルも存在する
- それぞれ違うメソッドに対して模倣を行っているため、スタブは検証してはならないというルールに違反していない
- 両方の性質を備える場合はモックと呼ばれる。
- スタブよりもモックであることのほうが重要なため。
- モックとスタブはコマンド・クエリ分離の原則と関連性がある。
- コマンド:副作用を起こす、戻り値なし -> モック
- クエリ:副作用なし、戻り地を返す -> スタブ
- 可能な限りコマンド・クエリの分離の原則に従う方が良い。
- コードの可読性が上がるため。
- 可能な限りコマンド・クエリの分離の原則に従う方が良い。
- すべてのプロダクションコードは次の2つの観点で分類できる
- 公開された API、プライベートな API
- 間作可能な振る舞い、実装の詳細
- 観測可能な振る舞いは以下のどちらかでなくてはならない。それ以外は実装の詳細になる。
- クライアントが目標を達成するために使う公開された操作
- クライアントが目標を達成するために使う公開された状態
- 実装の詳細が漏洩しているかの見分け方
- 1つの目標を達成するためにテスト対象となるクラスが提供する複数のメソッドを呼び出しているとき。
- いかなる目標であれ、1つの操作で目標を達成できるようにすべき。
- 1つの目標を達成するためにテスト対象となるクラスが提供する複数のメソッドを呼び出しているとき。
- API をきちんと設計すれば、単体テストは自然と質の良いものになる。
- ヘキサゴナルアーキテクチャ
- ドメイン層とアプリケーションサービス層との関心の分離
- ドメイン層はアプリケーションのドメイン知識を集めたもの
- アプリケーション層はビジネスにおけるユースケースを集めたもの
- アプリケーション内でのコミュニケーション
- 依存の流れがアプリケーションサービス層からドメイン層への一方向となる
- 外部アプリケーションとのコミュニケーション
- 外部アプリケーションとのコミュニケーションはアプリケーションサービス層にある共通のインターフェイスを介して行う。
- ドメイン層とアプリケーションサービス層との関心の分離
- モックを無差別に使うロンドン学派のスタイルはテストケースの実装の詳細と結びつけてしまい、リファクタリングの耐性を損なわせてしまう。
第6章 単体テストの3つの手法
- 単体テストの3つの手法
- 出力値ベース・テスト
- 状態ベース・テスト
- コミュニケーション・ベース・テスト
- 退行に対する保護、迅速なフィードバックの観点において、単体テストの手法による違いはほとんどない。
- リファクタリングへの耐性(上から順)
- 出力値ベース・テスト
- 状態ベース・テスト
- コミュニケーション・ベース・テスト
- 保守のしやすさ(上から順)
- 出力値ベース・テスト
- テストコードが最小になる
- 状態ベース・テスト
- 状態を見る分テストコードが増える
- 値オブジェクトを用いることで、比較を楽にすることができる。
- コミュニケーション・ベース・テスト
- モックが増えることでテストコードが増える
- 出力値ベース・テスト
- 基本的にすべてにおいて出力ベース・テストを用いるように努める必要がある。
- しかし、そんなに簡単なことではない。
- 純粋関数で構成されるようにすることで、達成できる様になる。
関数型アーキテクチャ
- 副作用をまったく発生させないアプリケーションを構築することは不可能。
- できたとしても使い物にならない。
- 関数型プログラミングが目標としているところは、副作用を取り除くことではなく、副作用を起こすコードを分離すること。
- 関数型アーキテクチャでは、副作用をビジネスオペレーションの最初や・最後に持っていくことでビジネスロジックと副作用を分離しやすくしている。
- 決定を下すコード
- 副作用を起こす必要がないため数学的関数を使って書く
- 関数的核と呼ばれる
- 決定に基づくアクションを実行するコード
- 数学的関数によって下された決定を観察可能な振る舞いの一部(DB の操作やメッセージの送信)に変換する
- 可変殻と呼ばれる
- これら2つの層の分離を適切に維持するために
- 可変殻は可能な限り指示されたことだけしか行わないような作りにする。
- 単体テストは関数的核だけを検証を行う
- 統合テストで可変殻の検証を行う
- 関数型アーキテクチャでは、すべての副作用を関数的核の外に出し、ビジネスオペレーションの最初や最後に持ち込むようにする。
- そして、服用に関する処理を可変殻で行うようにする。
- ヘキサゴナルアーキテクチャでは、ドメイン層内に限定される限り、副作用を起こすことが許されている。
- ドメイン層にあるクラスのインスタンスは DB に対して直接操作を行うことはしてはいけないが、そのインスタンス自体の状態を変えることは許されている。
- 関数型アーキテクチャの欠点
- 関数的核を呼び出す前にすべての入力値を集めるのが難しい場合
- 関数的核メソッドに DB 操作のオブジェクトを入れると純粋関数ではなくなってしまう。
- 関数的確に属するクラスは協力者オブジェクトと共に処理を行うのではなく、その協力者オブジェクトによって得られた結果を用いて処理をするようにする。
- 協力者オブジェクト:
- 可変であること
- まだメモリ上にはないデータの橋渡しをするものであること
- 協力者オブジェクト:
- パフォーマンスによる欠点
- 最初に必要なデータを全て取得したうえで実行する必要が出てくるため。
- パフォーマンスが重視されないシステムであれば、関数型アーキテクチャを優先したほうが保守がしやすくなる。
- コードベースが大きくなってしまう欠点
- 関数的核と可変殻を明確に分離するようにしているため、コード量が多くなる。しかし最終的にはコードの複雑さは減る。
- シンプルなシステムの場合、関数型アーキテクチャを導入する負担に見合う結果が帰ってこない場合がある。
- システムがどのくらい複雑になるかを検討して採用するか検討する。
- 関数的核を呼び出す前にすべての入力値を集めるのが難しい場合
第7章 単体テストの価値を高めるリファクタリング
- プロダクションコードに手を付けることなく、テスト・スイートを劇的に改善することはできない。
- テストコードとプロダクションコードは本質的に影響し合うものだから。
- プロダクションコードには「コードの複雑さ/ドメインにおける重要性」、「協力者オブジェクトの数」の観点から4種類に分けられる
- ドメイン・モデル/アルゴリズム
- 過度に複雑なコード
- このコードをなるべく減らす必要がある。
- 「ドメイン・モデル/アルゴリズム」か「コントローラ」に分割していく
- 取るに足らないコード
- コントローラ
- 質素なオブジェクト(Humble Object)
- 複雑なコードからテストを行いやすい部分を抽出し、抽出された部分を包み込むクラスを指す。
- 質素なクラスに対してテストをすることが難しい依存を結びつけるようにする。
- 質素なクラスに対してロジックをほぼ含ませないようにし、テストする必要が無いようにする。
- 質素なオブジェクトは単一責任の原則を遵守する手段としても見られる。
- ビジネスロジックに関するコードと連携の指揮に関するコードを分離することは重要。
- それぞれの責務は「コードの深さ(複雑さや重要性)」と「コードの広さ(協力者オブジェクトの数)」として考えられる。
- コントローラはコードの広さを責務とする
- ドメインクラスはコードの深さを責務とする
- 変換ロジックが複雑なロジックとして見られる理由
- 使う側からは見えない分岐がたくさん含まれている可能性がある。
- e.g.) フレームワークの内部で型変換を行う。
- 使う側からは見えない分岐がたくさん含まれている可能性がある。
- ドメイン層からプロセス外依存を取り除いた状態を維持することは簡単ではない。いくつかのトレードオフが求められる。
- 途中で得た結果を使ってプロセス外依存から新たにデータを取得しなければならない課題の解決法
- 外部依存に対する全ての読み込み書き込みをビジネスオペレーションの始めや終わりに持っていく
- パフォーマンスが落ちる
- ドメインモデルにプロセス外依存を注入する
- ドメインモデルのテストが複雑になる
- 決定を下す過程をさらに細かく分割する
- コントローラが複雑になる。
- -> この複雑さの解消が一番行いやすい
- コントローラが複雑になる。
- 外部依存に対する全ての読み込み書き込みをビジネスオペレーションの始めや終わりに持っていく
- トレードオフになる3つの性質
- ドメイン・モデルのテストのしやすさ
- コントローラの簡潔さ
- パフォーマンスの高さ
- 確認後実行(CanExecute/Execute)パターンの適用
- 確認後実行パターン:何かを実行するメソッドに対して、実行可能化を確認するメソッドを用意することで、対象の処理を正しく実行するための事前条件が必ず満たされることを保証する設計パターン。
- ドメインクラスに確認後実行メソッドを実装し、Controller クラスやビジネスロジックメソッドのどちらでも実行するようにする。
- Controller クラスでは、その実行条件を知る必要がなくなる。
- ビジネスロジックメソッドでは、実行条件を満たさないまま実行されることを防ぐ。(カプセル化)
- ドメインイベント:外部システムに伝えなくてはならないデータを含んだクラス
- ドメインイベントの名前は過去形になる。
- ドメイン・イベントは値であるため、不変オブジェクトとして定義する。
- ドメイン・イベントを導入することで、コントローラから決定を下すことに関する責務を抽出し、その責務をドメインモデルに担わせられる様になる。
- ドメインクラスからすべての協力者オブジェクトを取り除けることはめったにない。
- ただ、1つ2つ協力者オブジェクトが増えたとしても、プロセス外依存でなければあまり問題ではない。
- しかし、協力者オブジェクトとのやり取りはモックを使わないようにする。
- このような場合、ドメインモデルの視察可能なふるまいとは関係のないことだから。(実装の詳細になる)
- メソッドが視察可能なふるまいの一部になる条件は以下のどちらか
- 対象のメソッドがクライアントの目標の1つに直接結びついている。
- 外部アプリケーションから確認できる副作用がプロセス外依存で起こる。
- 各層のテストは1つ上の層の支店で行い、テスト対象となる層がその下にある層と何をしているかについては意識しないようにする。
第3部 統合(integration)テスト
第8章 なぜ、統合(integration)テストを行うのか?
- 統合テストは単体テストの性質を1つでも残っているテストが分類される。
- 一般的に単体テストはビジネスシナリオにおける以上ケース(edge case)をできるだけ多く検証するのに対し、統合テストは1件のハッピーパスと単体テストでは検証できない全ての以上ケースを検証することが適切だと考えられている。
- 単純なアプリケーションであっても、統合テストの価値が下がることはない。
- コードが単純であっても検証することの重要さは変わらないため。
- 統合テストを作成する場合はすべてのプロセス外依存とのやり取りを検証できるような長いハッピーパスを見つけ出す。
- 1つだけで網羅できない場合はケースを増やす。
- 早期失敗(Fail Fast)を利用すると統合テストの代わりに単体テストで検証を行うことができるようになる。
- 管理下にある依存に対しては実際のインスタンスを管理下にない依存に対してはモックを使うようにする。
- 管理下にない依存は後方互換を維持する必要があるためモックを使うほうが保守しやすい
- 統合テストで実際のデータベースが使えない場合
- データベースをモック化し統合テストを作るのではなく、統合テストは作成しないようにし、単体テストの作成に専念する。
- この場合統合テストを作成しても検証できることが少なく、単体テストと同じくらいの保護しか提供することはできない。
- また、リファクタリングへの耐性や退行に対する保護も失われてしまう。
- データベースをモック化し統合テストを作るのではなく、統合テストは作成しないようにし、単体テストの作成に専念する。
- DB の状態を確認する際、検証するデータが入力値として使ったデータとは異なる経由で取得したものにすることが大切
- それによって、書き込みと読み込みどちらの検証も行えていることになる。
- インタフェースの誤った使用理由
- プロセス外依存を抽象化でき、疎結合を実現するため
- 実装クラスが1つしか無いのであれば、インタフェースは抽象ではない。
- そのまま具象クラスを使う場合と比べて疎結合になるわけではない。
- 既存コードを変更することなく、新しい機能を追加できるようになるため(OCP を遵守)
- YAGNI の原則に外れるため NG。
- プロセス外依存を抽象化でき、疎結合を実現するため
- インタフェースを実装クラスが1つにしかないのに、プロセス外依存に用いる理由。
- モック化するため。
- プロセス外依存をモックにする必要がなければ、インタフェースを作るべきではない。
- 煮詰めると、管理下に無い依存に対してのみインタフェースを用意する。
- 管理下にある依存に対しては具象クラスを使うほうが望ましい。
- 注意するのは、インタフェースに対して複数の実装クラスができるのであれば、モックにするか関係なく使用して良い。
- 同一プロセス内の依存に対するインターフェイスの使用
- モックへの置き換えを行うために作られるものだが、リファクタリングへの耐性を失うため使用しない方がよい。
- 実装の詳細を検証していることになるため。
- モックへの置き換えを行うために作られるものだが、リファクタリングへの耐性を失うため使用しない方がよい。
- 統合テストのベスト・プラクティス
- ドメイン・モデルの境界を明確にする
- ドメインクラスとコントローラとの境界が明確になっていると単体テストと統合テストの区別をしやすくなる。
- 単体テストのテスト対象:ドメインモデルとアルゴリズム
- 結合テストのテスト対象:コントローラ
- ドメインクラスとコントローラとの境界が明確になっていると単体テストと統合テストの区別をしやすくなる。
- アプリケーションを構成する層を減らす
- コードの抽象化や汎用化を行う際、間接参照の層が増えがち。
- この層が増えるとコードの理解が難しくなる。
- バックエンドシステムであれば、殆どの場合、ドメイン層、アプリケーションサービス層、ドメイン層の3つの層で十分なはずなので、これを保つようにする。
- 循環依存を取り除く
- 循環依存はコードの理解を難しくする。
- 循環依存を解決する際にインタフェースを用いた抽象化を行うのは正しくない。
- 根本的な解決担っていないため。
- 循環依存を取り除くことにフォーカスを当てる。
- ドメイン・モデルの境界を明確にする
- 1つのテストケースに複数の実行(Act)を定義してはいけない。
- 何を検証しているか曖昧になり、テストの肥大化が発生する。
- ただし、プロセス外依存に制約がある(e.g. 回数制限)場合は容認しても良い。
ログ出力に対するテスト
- ログ出力をテストすべきか
- アプリケーションの観察可能な振る舞いか、実装の詳細かによって変わる。
- 外部から見られることを意図している場合は、テスト対象となる。
- サポートログと呼ぶ
- 開発者だけが見る尾久の場合は、テスト対象とならない。
- 実装の詳細にあたるため。
- 診断ログと呼ぶ
- 外部から見られることを意図している場合は、テスト対象となる。
- アプリケーションの観察可能な振る舞いか、実装の詳細かによって変わる。
- 構造化ログ:ログ・データの生成とそのデータの出力とを切り離すログ出力のテクニックのこと。
- サポートログと診断ログの Logger クラスは分ける。
- プロセス外依存となるサポートログの Logger クラスはドメインクラスでは呼び出さない。
- ドメイン・イベントと同様にコントローラクラスにプロセス外依存の呼び出しを移譲する。
- ログの出力の量について
- 過度なログ出力は控える
- プロダクションコードが汚くなる。
- ノイズとなる情報が多くなる。
- ドメインモデルでは診断ログを出力しないようしたほうがよい。
- ドメインクラスで出力させているログはコントローラに出力させるようにする。
- 過度なログ出力は控える
- ログ出力オブジェクトの受け渡し方
- static メソッドからログオブジェクトを取得できるようにする。→ 非推奨。
- これは環境コンテキスト(ambient context)というアンチパターンとされている。
- 依存関係が隠れてしまい、変更が難しくなる
- テストがより難しくなる。
- コンストラクタやメソッドの引数から受け渡す。
- これは環境コンテキスト(ambient context)というアンチパターンとされている。
- static メソッドからログオブジェクトを取得できるようにする。→ 非推奨。
第9章 モックのベストプラクティス
- モックする対象はアプリケーションの境界となるもの(依存に向かう最後のコンポーネント)にすると最大限の価値を得られる。
- 退行に対する保護が強まる
- 様々なコンポーネントを経由した検証を行うことができるため。
- テストケースに備わるリファクタリングへの耐性が強力になる。
- コードとの結びつきが弱くなるため。
- モックのために準備していたアプリケーション内の不要なインターフェイスを削除できる。
- 退行に対する保護が強まる
- スパイ:モックと同じ目的で使われうテストダブルの一種。
- 唯一の違いは、モックはフレームワークの助けを借りて作成され、スパイは手がきで実装される点。
- 作成するテストダブルの対象がアプリケーションの境界に位置する場合、モックよりもスパイのほうが優れている
- 実行結果を確認するためのコードをスパイ上に記述でき、様々なテストケースから利用できる。
- 確認する内容を一箇所にまとめられて、可読性の向上が期待できる。
- スパイはテストコードの一部となる
- テストで確認をする際はプロダクションコードを信頼すべきではない。
- プロダクションコード似定義されたリテラルや定数を使わない。
- テストで確認をする際はプロダクションコードを信頼すべきではない。
- 実行結果を確認するためのコードをスパイ上に記述でき、様々なテストケースから利用できる。
- ログ出力の検証ではアプリケーションの教会をモックすることを意識する必要はない。
- プロセス外依存の出力はいかなる変更も許されないことが期待されている。
- そのため、アプリケーションの境界をモックすることが期待される。
- ログ出力の結果は、意図したログ情報が含まれていれば十分。
- そのため、モックの対象をもう少し内側にしても十分保護可能。
- プロセス外依存の出力はいかなる変更も許されないことが期待されている。
- モックのベストプラクティス
- モックの利用は統合テストに限定する
- テスト対象とするコードは以下
- プロセス外依存とのやり取りを行うコード
- 複雑さを持つコード
- プロダクションコードでは前者はドメインモデルの層、後者はコントローラの層に分かれる。
- ドメインモデルの検証は単体テストで行い、統合テストの検証はコントローラで行われることが望ましい。
- テスト対象とするコードは以下
- 1つのテストケースには複数のモックを持たせてはならないという誤解
- テストでは1単位の振る舞いを検証することが求められる。
- その際、複数のプロセス外依存を持つことは考えられる。
- モックの呼び出し回数を常に確認する。
- 管理下にない依存とのコミュニケーションを検証する際に重要なこと
- 想定する呼び出しが行われていること
- 想定する呼び出しが行われていないこと
- 管理下にない依存とのコミュニケーションを検証する際に重要なこと
- モックの対象になる方は自身のプロジェクトが所要する型のみにする。
- つまり、サードパーティのライブラリが提供するものを直接モックするのではなく、アダプタを独自に作成し、それをモックするようにする。
- この指針はプロセス外依存に対して有効であり、同じプロセス内の依存に対してはそのままライブラリを用いるべきである。
- モックの利用は統合テストに限定する
第10章 データベースに対するテスト
- データベースをテストするのに必要な事前準備
- スキーマを Git で管理する
- スキーマに参照データ(reference dta)を含めること
- 開発者ごとに個別のデータベースインスタンスを用意すること
- 状態ベースではなく移行ベースで本番環境へ反映すること
- 状態ベース:比較ツールを用いて本番環境との差分を埋め合わせる SQL スクリプトを生成し反映する。
- 移行ベース:データベースに対して何を行ったのかを、開発者自身が本番環境に変更を反映する SQL スクリプトを作成する。
- それぞれトレードオフがあるが、移行ベースのほうがデータモーションの取り組みが容易となるため望まれる。
- 通常、データモーションの取り込みのほうがはるかに重要となるため。
- データモーション:既存のデータの形状を変え、新しくなったスキーマに適用するようにすること。(パッチ)
- データベース操作とトランザクションの分離
- データを更新する際は2つの決定を明確に分ける
- 度のデータを変更するのか。
- 変更したデータを反映するか。
- これらはコントローラで同時に下すことはできないため、以下のクラスに責務を分ける
- リポジトリクラス
- トランザクションクラス
- データを更新する際は2つの決定を明確に分ける
- トランザクションを単位作業(Unit of Work)に変換する
- 単位作業(Unit of Work):1つのビジネスオペレーションの変更をオブジェクトが全て保持し、ビジネスオペレーションが完了するときにまとめて DB 似反映するパターン。
- 更新データの輻輳を減少できる
- DB の呼び出し回数を減らせる
- 統合テストにおけるトランザクションは実際のユースケースに揃える必要がある。
- フェーズによってそれぞれ異なるトランザクションを用いる必要がある。
- 統合テストは1つずつ実行すべき
- 並行に行うと他のテストに影響を与えてしまうため。
- パフォーマンス改善のために並行稼働させることもできなくないが、コスパが悪いため非推奨。
- 統合テストでのデータの後始末の方法
- 各テストケース実行前にバックアップから DB を復元
- テスト時間が長くなる。
- テストケースの実行後にデータの後始末をする
- ケース実行中にサーバーエラーが発生した場合、データ削除が行われず不整合が起きる。
- 各テストケースを1つのトランザクションで行い、コミットせずロールバックをする
- プロダクションコードで行われるトランザクションの流れと一致しないため正確な検証ではなくなる
- テストケースの実行前にデータの後始末を行う。★推奨
- 上記の方法の欠点がカバーできる。
- 各テストケース実行前にバックアップから DB を復元
- メモリ内 DB を使用するという方法もあるが、非推奨。
- 一般的な DB とは異なる性質があるため、適切な検証が行えない。
- テストコードの再利用
- 準備(Arrange)フェーズでのコードの再利用
- 同じようなコードをプライベートなファクトリメソッドに抽出する。
- オブジェクトマザー:テストを実施するのに必要なオブジェクトであるテスト・フィクスチャの生成を助けるクラスやメソッド
- テストデータビルダー:オブジェクトマザーと似ているが、メソッドではなく、Fluent Interface を採用した API を提供する。
- テストデータビルダーのほうが読みやすくはなるが、ボイラープレートコードが増えるため、オブジェクトマザーのほうが推奨される。
- ファクトリメソッドは最初はテストクラスの private メソッドで作成する。
- 重複が目立ってきたら個別のヘルパークラスに移すことを検討する。
- 基底クラスには配置しない。
- 基底クラスにはすべてのテストメソッドで呼ばれるコードだけを置く。
- 実行(Act)フェーズでのコードの再利用
- テスト対象システムの機能への呼び出しが移譲されるメソッドを導入する。
- トランザクションの実行を移譲できる。
- テスト対象システムの機能への呼び出しが移譲されるメソッドを導入する。
- 実行(Assert)フェーズでのコードの再利用
- 実行結果のデータ取得にヘルパーメソッドを定義する。
- 準備(Arrange)フェーズでのコードの再利用
- トランザクションが多くなるのは、「迅速なフィードバック」と「保守のしやすさ」のトレードオフ。
- 通常開発者のマシーン上に DB インスタンスをホストできるのであれば、トランザクションが少し増えることは大した劣化にはならないため、保守のしやすさを優先した方が良い。
- DB 読み込みに対するテストの重要性は低い
- バグがあったとしても大きな外をもたらす可能性が低い。
- 複雑なロジックや重要な役割を担う場合にテストをする。
- 読み込みに関して、ドメインモデルは必要ない。
- ドメインモデルで達成したいことは1つのカプセル化。
- カプセル化とはデータの変更が行われてもデータの整合性が保たれるようにするための手法。
- データの変更がない場合、カプセル化をする必要もなくなる。
- ドメインモデルで達成したいことは1つのカプセル化。
- リポジトリはテストをすべきか。
- リポジトリはプロセス外依存を扱うため、コントローラに属する。
- リポジトリに複雑なコードが含まれる場合は、別クラスに分離しそれを検証した方が良い。
- e.g.) ドメインクラスを生成するファクトリクラス等
- ただ、ORマッパーを使っている場合は分離することが難しい。
- その場合リポジトリは統合テストのシナリオの一部にして、検証することが望ましい。
- リポジトリを直接テストすることは、保守コストを高くしてしまい、労力に見合った退行に対する保護を得られる可能性が低い。
第11章 単体テストのアンチパターン
- 原則プライベートメソッドはテスト対象とするべきではない。
- プライベートメソッドは実装の詳細となっているため。
- 観測可能な振る舞いに対する検証は十分にされているのに網羅率が高くならない場合いかが考えられる
- デッドコード
- 未使用のコードのため、削除する。
- 抽象化の欠落
- プライベートメソッドがあまりに複雑になっている。
- 抽象化できる部分を別のクラスに抽出する
- デッドコード
- プライベートでありながらも観察可能な振る舞いの一部になることもある。
- e.g.) O/Rマッパーやファクトリクラスがインスタンスを生成するときに呼び出すプライベートなコンストラクタ。
- その場合は公開して良い。それによってそのクラスの API が破綻することはない。
- テストためだけにプライベートなフィールドをパブリックにしてはいけない。
- 検証では本番環境と全く同じ方法でテスト対象のコードとやり取りをする必要があるため。
- テストコードにプロダクションコードのロジックを記載し期待値としてはいけない。
- これはプロダクションコードをコピペしたものなので、検証の価値がなくなる。
- また、リファクタリングへの耐性もなくなる。
- テストを作成する際は、期待値に直接値を書き込む。
- テストでのみ必要とされるコードをプロダクションコードに記載してはいけない。
- 具象クラスをテスト・ダブルにしてはいけない。
- 具象クラスをテストダブルにする目的は一部の既存機能をそのまま使えるようにし、一部の機能をスタブ化したいから。
- だが、そうしなくてはならないのは、単一責任の原則が遵守できていない可能性が高い。
- 具象クラスをテストダブルにする目的は一部の既存機能をそのまま使えるようにし、一部の機能をスタブ化したいから。
- 現在日時を単体テストで扱いたい場合、明示的な依存として扱う。
- サービスもしくは値として現在日時を注入する。
- 可能な限り値として注入することが望ましい。
- サービスもしくは値として現在日時を注入する。
このスクラップは2024/05/12にクローズされました