Open38
[読書]単体テストの考え方/使い方
第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 に対して直接操作を行うことはしてはいけないが、そのインスタンス自体の状態を変えることは許されている。