はじめに
この記事は、「単体テストの考え方/使い方」という本を読んだときのメモです。
まだ読書途中なので、随時加筆修正していきます。
この本を読みたいと思ったきっかけ
日々TDDをしながらアプリ開発をしていく中で、「実装コードを書くためにテストが必要だからテストを書く」ことに対して、何かよくわからないけどもやもやした気持ちになることが増えた。
このテストについてもっと本質的なことを知りたくなった。
そんなとき、「単体テストの考え方/使い方」という本に出会った。
少し読んでみた感想
今まで点でしかなかった知識が線でつながる感覚にワクワクが止まらない!
技術書なのにどんどん読み進められちゃうほど面白い!!
読書メモ
第1章 なぜ、単体(unit)テストを行うのか?
- ソフトウェア開発プロジェクトの成長を「持続可能」なものにするため
- テストの質が悪いと
- テストが間違った理由で失敗することが頻繁に起こる
- バグを検出できない
- テストに時間がかかりすぎて簡単に保守できなくなる
- 作成する単体テストの価値とその維持にかかるコストの両方を考慮する必要がある
- プロダクションコードのリファクタリングに伴って、テストコードをリファクタリングする
- プロダクションコードを変更するたびにテストを実行する
- テストが間違って失敗したとき、その対処をする
- プロダクションコードがどのように振る舞うのかを理解するために、テストコードを読む
- 網羅率(coverage)はテストコードの質が悪いことを判断するには効果があるが、テストコードの質が良いことを判断することには向かない
- 何がテストコードの質を良くするのか?
- テストすることが開発サイクルの中に組み込まれている
- コードベースの特に重要な部分のみがテスト対象となっている
- 最小限の保守コストで最大限の価値を生み出すようになっている
(感じたこと)
テストの質とかあんまり考えたことなかったけど、テストにも質があるのか💡
テストコードのリファクタリングってどうしたらいいかまだよくわかってない・・・
第2章 単体テストとは何か?
- 単体テストの定義
- 「単体(unit)」と呼ばれる少量のコードを検証する
- 実行時間が短い
- 隔離された状態で実行される
- 「隔離」の捉え方の違いから、「古典学派(デトロイト学派)」「ロンドン学派」が自然発生した
- 著者は古典学派を好んでいる
- ロンドン学派が考える隔離
- テスト対象システム(System Under Test)から協力者オブジェクト(collaborator)を隔離すること
- テスト対象のクラスが他のクラスに依存しているのであれば、その依存をすべてテストダブルに置き換えなくてはならない
- 古典学派が考える隔離
- テストケース同士を隔離すること
- 各テストケースがお互いに影響を与えることなく個別に実行できなくてはならない
- ロンドン学派が考える単体
- 1単位のコード(クラス)
- 古典学派が考える単体
- 1単位の振る舞い
- ロンドン学派の課題
- 検証内容が詳細になりすぎてしまう
-> 単体テストがテスト対象の内部的なコードと密接に結びついてしまう - 一部のコードを簡単にテストできないとき、コードの設計に問題があることを強く示唆しているが、テストダブルを使ってテストができるようにしても根本的な問題は解決しない
- 検証内容が詳細になりすぎてしまう
(感じたこと)
単体テストの定義ってあったのね。知らずに書いてたよ・・・実行時間とか気にしたことなかったし・・・
ロンドン派とデトロイト派があったのは知ってたけど、「隔離」に対する考え方の違いで自然発生したとは知らなかった〜!
今までロンドン派でテストを書いてると思ってたけど、テストするクラスも隔離してるけど、テスト同士も隔離するようにしてるから、いいとこどりしながらテストを書いてたのかな・・・?
著者がデトロイト派なだけに、ロンドン派の課題が言語化されていて、その辺りも気にしながらテストを書こうって思えるようになったのは収穫かも。
第3章 単体テストの構造的解析
- AAA パターン
- 準備(Arrange)
- 実行(Act)
- 確認(Assert)
- AAA パターン ≒ Given-When-Then パターン
- TDD を取り入れている場合、確認フェーズから書き始めることがある
- 開発する機能の振る舞いをまだ十分に把握していないことがある
- その機能がどう振る舞うべきなのかを最初に考え、次にどう実現させるかを考えると考えやすい
- 単体テストで回避すべきこと
- ひとつのテストケースの中に複数の同じフェーズがある(Given-When-Then-When-Then-When-Then)
- ひとつのテストケースの中で複数の振る舞いを検証している -> 統合テストに属する
- テストの実行時間が長くなる -> 単体テストの定義、実行時間が短いに反する
- テストを理解するのに時間がかかる -> 単体テストのテストケースは簡潔で簡単に理解できるものであること
- テストケースを分割して、ひとつの実行フェーズとひとつの確認フェーズを持つテストケースを複数作成する
- if 文の使用
- 単体テスト、統合テストに関わらず、分岐のない単純な流れにしなくてはならない
- 分岐がある -> 複数のことを検証しようとしている
- テストが読みづらくなる -> 理解しづらいものになる -> テストに対して余分な保守コストがかかるようになる
- ひとつのテストケースの中に複数の同じフェーズがある(Given-When-Then-When-Then-When-Then)
- 準備フェーズが、実行フェーズと確認フェーズを合わせたサイズよりはるかに大きくなる場合
-> その一部を同じテストクラスのプライベートメソッドに切り出す or 別のファクトリクラスを作る - 実行フェーズが1行を超す場合は注意が必要
-> APIがきちんと設計されていないことを示唆している - 確認フェーズが大きくなり過ぎる場合
-> プロダクションコードでの抽象化がうまくいっていない可能性が高い - テストケースから各フェーズを示すコメントを取り除くか否か
- 3つのフェーズが明確に区別しやすくなっていることが重要
- 各フェーズ内のコードを書くのに空白行が不要で、各フェーズを空白行で区切れるのであればコメントを取り除いてOK
- ひとつのフェーズで空白行を用い処理をまとめる場合、3つのフェーズが不明確になるため、各フェーズを示すコメントは残す
- 複数のテストケースで同じコードを共有する場合
- 複数のテストケースで準備フェーズのコードが同じでも beforeEach に切り出さない
- 各テストケースが読みづらくなる
- テストケース間の結びつきが強くなってしまう -> ひとつのテストケースに関する修正が他のテストケースに影響を与えてはいけない
- 複数のテストケースで準備フェーズのコードが同じでも beforeEach に切り出さない
- テストタイトルを付けるとき
- 厳格な命名規則に縛られないようにする
- 問題領域のことに精通している非開発者に対してどのような検証をするのかが伝わるような名前をつける
- テスト対象のメソッド名を含めない
- リファクタリングでメソッド名を変更したとき、テストタイトルも修正する必要がある -> テストに対して余分な保守コストがかかるようになる
- パラメータ化テスト
- メリット:テストコードの量を劇的に減らせるようになる
- デメリット:このテストが何の事実を表現しているのか分かりづらくなる
(感じたこと)
Given-When-Then-When-Then-When-Then・・・
これ、やってるなぁ〜。
確かに何が検証したいのか、ぱっと見て分からない。
// Given
// When
// Then
このコメントは残してもいいやつだったんだ!!
無理して消してたけど、これからは残しておこう📝
ちょっとずつ、テストの質ってやつが分かってきた気がする。
多分、今開発してるプロダクトのテスト・・・スメってるカモ🦆
気づいたら Given が beforeEach にまとまってて、各テストケースから Given が消えてたし😅
この本読んでなかったら、私も同じことやってたかもな〜・・・テストのリファクタリングって難しい。。。
パラメータ化テストは知らなかった。三角測量に使うとよさそう!
第4章 良い単体テストを構成する4本の柱
- 4本の柱
- 退行(regression, バグ)に対する保護
- リファクタリングへの耐性
- 迅速なフィードバック
- 保守のしやすさ
- 退行(バグ)に対する保護 - 気に掛けるポイント -
- テストの際にできるだけ多くのプロダクションコードを実行させる
- 複雑なビジネスロジックが記述されたコード
- ビジネス的に重要な機能
- 使用しているライブラリやフレームワーク
- 偽陰性:テストが成功したにも関わらず、検証された機能に欠陥がある(退行に対する保護)
- 偽陽性:意図した振る舞いであるにも関わらず、テストが失敗する(リファクタリングへの耐性)
- 真陰性:テストが成功し、テスト対象も正しい振る舞いをしている
- 真陽性:テストが失敗し、テスト対象の機能も間違った振る舞いをしている
- リファクタリングへの耐性
- テストが失敗することなく、どのくらいプロダクションコードのリファクタリングを行えるか
- リファクタリングを行っても、偽陽性が生まれづらい性質のこと
- 偽陽性が起きる要因
- テストコードがプロダクションコードに密接に結びついている
- 退行に対する保護とリファクタリングへの耐性を備えることはテストスイートの正確性を最大限に引き上げることになる
- 迅速なフィードバック
- テストの実行時間が短い = フィードバックを得てから改善するまでの時間が短くなる
- 保守のしやすさ
- テストケースの理解のしやすさ
- テストケースのサイズが小さいほど、テストコードは読みやすい → 変更もしやすい
- テストコードの品質はプロダクションコードの品質と同じくらい重要
- テストを実施する難しさ
- テスト時に使われるプロセス外依存が少ないほど、テストの実施は簡単になる
- テストケースの理解のしやすさ
- 4本の柱すべてを最大限に備えることは不可能
- 退行に対する保護とリファクタリングへの耐性と迅速なフィードバックは互いに排反する性質を持っている
- 最善の単体テストとは?
- リファクタリングの耐性と保守のしやすさを最大限に備え、退行に対する保護と迅速なフィードバックの間でバランスをとる
- テストスイートを堅牢にするために最も優先すべきこと
- 偽陽性を取り除くこと
(感じたこと)
テストを書く理由として、既存の機能が失われていないかを確認できるので、自信を持ってリファクタリングや新しい機能を追加できるから。というのが今までの認識だった。間違ってはいないと思うけど、これだけじゃ要素としては足りなくて、ただテストを書けばよいのではなく、「偽陽性が生まれづらい」テストを書くからこそ、自信を持ってリファクタリングや新しい機能を追加できるのかなと思った。
今のところグリーンになるまで次のコードを書かないことは徹底しているので、レッドの状態を放置していないことはよい状態かなと思う。しかし、ハッピーケースのテストが多く、偽陰性が潜んでいる可能性は否定できない。
第5章 モックの利用とテストの壊れやすさ
- テストダブルは大きく2つに分けることができる
- モック(モック、スパイ)
- スタブ(スタブ、ダミー、フェイク)
- モック(モック、スパイ):テスト対象システムから外部に向かうコミュニケーション(出力)を模倣し、検証するのに使う
- スタブ(スタブ、ダミー、フェイク):テスト対象システムの内部に向かうコミュニケーション(入力)を模倣する(検証には使わない)
- モックとスパイの違い
- モック:モックフレームワークの助けを借りて生成
- スパイ:開発者自身の手で実装される(手書きのモック)
- スタブとダミーとフェイクの違い
- ダミー:null 値や一時しのぎで使われる文字列などのシンプルなハードコーディングされた値
- スタブ:設定によって返す結果を異なるシナリオごとに変えられる完全に自立した依存として振る舞う
- フェイク:まだ存在しない依存を置き換えるために作成される
- スタブとのやり取りを検証するのはアンチパターン
- スタブはテスト対象システムが最終的な結果を生成するのに必要なデータを提供するだけで一過程にすぎない
- 実装の詳細と結びつきやすくテストが壊れやすくなる
- 過剰検証:最終的な結果の一部とはならないものを検証すること
- コマンド・クエリ分離の原則
- コマンド:副作用あり、戻り値なし・・・モック
- クエリ:副作用なし、戻り値あり・・・スタブ
- 一部例外はあるが、可能な限り従う
- すべてのプロダクションコードは2つの観点で分類できる
- 公開されたAPI or プライベートなAPI
- 観察可能な振る舞い or 実装の詳細
- 観察可能な振る舞い(必ずどちらかに該当する)どちらにも該当しないものが実装の詳細
- クライアントが目標を達成するために使う公開された操作
- クライアントが目標を達成するために使う公開された状態
- 理想は、システムが公開しているAPIが観察可能な振る舞いと一致し、そのシステムのすべての実装の詳細がクライアントから完全に隠れるようになっていること → APIをきちんと設計する必要がある
- カプセル化:不変条件の侵害からコードを守る手段
- 実装の詳細を隠すこと
- データを操作させるのにメソッドを経由させること
- テストケースを作成する際、どのようなビジネス要求があるのかをそのテストケースから分かるようにしなくてはならない
- アプリケーションが行うコミュニケーションには2種類ある
- システム内コミュニケーション → 実装の詳細
- システム間コミュニケーション → 観察可能な振る舞いの一部
- システム内コミュニケーションの確認にモックを使うことはテストを壊れやすくすることにつながる
(感じたこと)
テストダブルが大きく2つに分かれることは知らなかったけど、言われてみればそうかもな〜って感じがした。だけど、ダミーとスタブの違いは教えてもらったものとちょっとニュアンスが違うように感じた。スタブは固定値を返すもので、ダミーはコンパイルを通すためだけに使い、もしダミーが使われたときはエラーを返すイメージだった。
コマンド・クエリ分離の原則って初めて聞いたけど、確かにひとつの関数に副作用もあって戻り値もあったらややこしいかも。
スタブとのやり取りを検証するのはアンチパターンってあるけど、ロンドン派でテスト書いてるとあるあるな気がしなくもない・・・?気のせいかな・・・。
コードのカプセル化って意識して書いてないから、できてない部分が結構あるんじゃないかとふと思ってしまった。private とか public を意識的に書いた記憶がほとんどない・・・。
第6章 単体テストの3つの手法
- 単体テストの3つの手法
- 出力値ベーステスト(戻り値を確認) プロダクションコードが副作用のないコードであること
- 状態ベーステスト(状態を確認) 副作用で変化した状態を確認
- コミュニケーションベーステスト(オブジェクト間のやり取りを確認) 協力者オブジェクトをモックに置き換える
- 2つの学派の好み順
- 古典学派:出力値ベーステスト → 状態ベーステスト → コミュニケーションベーステスト
- ロンドン学派:出力値ベーステスト → コミュニケーションベーステスト → 状態ベーステスト
- リファクタリングへの耐性を維持するのに必要なコスト
- 出力値ベーステスト:低い
- 状態ベーステスト:普通
- コミュニケーションベーステスト:普通
- 保守のしやすさを維持するのに必要なコスト
- 出力値ベーステスト:低い
- 状態ベーステスト:普通
- コミュニケーションベーステスト:高い
- 関数型プログラミングの目標
- ビジネスロジックを扱うコードと副作用を起こすコードを分離すること
- 関数型アーキテクチャでは、副作用をビジネスオペレーションの最初と最後に持っていくことで、ビジネスロジックと副作用を分離しやすくしている
- ビジネスロジック:決定を下すコード、関数的核、不変核
- 副作用:決定に基づくアクションを実行するコード、可変殻
(感じたこと)
ビジネスロジックを扱うコードと副作用を起こすコードを分離することってあんまり意識したことないけど、コンポーネント(コントローラ)、サービス、リポジトリみたいな感じに分けて書くことがそういうことなのかな??この章話が難しかった〜。
単一の責務、疎結合・・・キーワードは分かるけど、いざコードにしようとすると、どう分ければいいのか?どうすれば疎結合になるのか??ピンともこないや・・・。
関数型プログラミングとか関数型アーキテクチャについてもう少し理解を深めたいな〜って気持ちになりました。ここら辺をもう少し意識的に書けるようになると、テストの質も上がるかも??