[読書メモ]単体テストの考え方・使い方をざっくり読んでみた
最初に
単体テストの考え方/使い方を読んでいる際に、個人的につけていたメモです。
いわゆるまとめ記事ではなく、内容も飛び飛びで自分の感想も多々混ざっていますが、ご了承ください。
新人エンジニアが読むべき本!という記事で大体おすすめされていますが、やっぱり良かったです!
第2章 単体テストとは何か
依存には共有依存とプライベートな依存がある。
共有依存とは、複数のテストケースにおいて共通で利用しているものを指し、プライベートな依存は一つのテストケースの中に依存が閉じているものを指す。
また、プライベートな依存の中でも可変依存と値オブジェクト(不変依存)に分かれる。可変依存とは、テストの実行中に状態が変わる可能性があるが、プライベートに閉じているるオブジェクトのことを指し、値オブジェクトはリテラルをはじめとした変わらないもの、すなわち不変なオブジェクトを意味する。
単体テストにおける隔離の定義をテスト対象のクラスとしているロンドン学派では、共有依存と可変依存に対してモックを当てる方針であり、逆に単体テストにおける隔離の定義を対象systemの振る舞いと定義している古典学派では、共有依存のみにモックを当てる方針となっている。
第3章 単体テストの構造的分析
テストコードは3A、すなわち準備(Arrange)、実行(Action)、確認(Assert)の3つの段階で記述すると可読性高く保つことができる。
実行におけるコードは通常一行であるべきであり、もし2行にわたる場合は、テスト対象のコードの範囲を間違えてしまっているか、もしくはテスト対象のコードの設計が悪いことを意味している。これはすなわち、テストしたい振る舞いを実行するための入り口が単一でないことを意味している。このような状態は不変条件の侵害と呼ばれる。
テストメソッドに名前を付ける場合は、厳格な名前を付けるべきではない。また、命名時に実行するメソッドの名前を含めてはならない。なぜならテストすべきは振る舞いであり、その振る舞いを実行する入り口が開発していく中で変化していたとしても、振る舞い自身が担保されていれば問題ないからである。もし、テストメソッドの名前にテスト対象のメソッドを含めてしまっている場合、改修によって実行対象のメソッドが変化した場合同時にテストメソッドの名前も変更する必要があり無駄な保守コストを増やす要因になりうる。
第4章 良い単体テストを構成する4つの柱
テストコードを実行した際、実際の挙動としては問題ないものの、テスト自体が失敗してしまっている場合、その結果は偽陽性であったといえる。偽陽性が高まると、テストに対する信頼がなくなってしまい、リファクタリングを進めることにちゅうちょするようになってしまう。
偽陽性を減らすためにはテストコードの中でコードの詳細部分を検査対象にしてはならない。検査対象がコードの詳細に寄りすぎると、テスト対象のコードに対してわずかな(いわゆる振る舞いを変化させない)改修であってもエラーとなってしまうためである。
つまり、テストケースを考える際に、テスト対象のコードが抱える責務は何か?そして責務を達成するにあたって求められる振る舞いは何かを考えてピンポイントでテストコードに落としていくことが重要となる。
そうした思考の上で実装されたテストコードは、リファクタリングの際の自由度と安全性をともに向上させる。
E2Eテスト、統合テスト、単体テストの話を見ていると、古典学派における統合テストと単体テストの違いが判らなくなったが、2章を見返すと以下の3つの定義の内、どれか一つが落ちているものは古典学派の定義で言うところの統合テストに当たると復習した。
- 1単位の振る舞いを検証すること
- 実行時間が短いこと
- ほかのテストケースから分離された状態で実行可能なこと
第5章 観察可能な振る舞いと実装の詳細
テストコードにおける検証はテスト対象のオブジェクトの外部から見た振る舞いをピンポイントで行うべきであり、詳細に寄りすぎるとリファクタリングへの耐性が低いコードとなってしまうという問題がある。そのためにはテスト対象のオブジェクトが、それをクライアントが達成したい目標を公開されたAPIを呼び出した際にどのような最終的な結果になっているかを考えることが必要となる。そしてそもそもの設計として、システムが公開しているAPIが観察可能な振る舞いと一致しており、なおかつそのシステムのすべての詳細がクライアント側から隠れている状態が理想となる。
システムの詳細が外部に漏洩していないかをチェックするための基準として、クライアントから対象のシステムをどのように呼び出しているかを確認する方法がある。例えば、クラスAのメソッドBを呼び出した後にメソッドCを呼び出す、という処理順序となっていた場合、実装の詳細が漏洩していると判断できる。そこから、そもそもクラスAの中でメソッドBとメソッドCを良しなに実行してくれるメソッドDを実装しておくべきと考えることもできる。
いかなる目標であれ、1つの操作で目標を達成できるAPIは観察可能な振る舞いと一致しているといえ、テストがしやすいコードと言える。自然と関心の分離も行われ、良いコードになる。
モックは外部に向かうコミュニケーション(出力)を模倣・検証する場合に用いて、スタブは内部に向かうコミュニケーション(入力)を模倣するために利用される。
第6章 単体テストの3つの手法
出力値ベースのテストが一番こわれにくく、素早く実行できて、保守性も高いものになりやすいという傾向がある。とはいえ既存のコードすべてが出力値ベースのコードになっているわけではない。そこで、純粋関数と副作用を含む処理を切り分けて構築するアーキテクチャである関数型アーキテクチャを採用することが既存のコードを出力値ベースのテストを書きやすい状態にするための方法として挙げられていた。
関数型アーキテクチャの話はあくまで一例であって、すべてに適応できるわけではない。大事なのは責務をちゃんと切り分けようということなのかもしれないと感じた。体感値として、副作用の有無という観点とは別で、各オブジェクトが担当すべき責務が何で、どのようにグルーピングできるか、すなわち責務・振る舞いを考えていると自然と関心事の分離ができて結果としてテストコートが書きやすい状態に近づくのではないかと素人ながらに思った(自信はない)
第7章 単体テストの価値を高めるリファクタリング
協力者オブジェクトとは、あるオブジェクトが自身の責務を果たすにあたってサポートを依存・委譲する相手のオブジェクトのことを指す。協力者オブジェクトがプロセス外依存の場合、テストケースの中で必然的にモックが用いられることになるため、リファクタリングへの耐性が失われてしまいがちとなる。そのため、プロセス外依存とのコミュニケーションはドメイン層に混ぜるのではなく、ドメイン層より外側のクラスに委譲することが重要となる。
リファクタリングを行うコードの選定をするにあたって、最も効果が高いのはコードの複雑さやドメインにおける重要性は高いものの、協力者オブジェクトの数が少ないコード、「ドメイン・モデル/アルゴリズム」になる。なぜならば、そのようなコードは機能させるために必要な準備が少なくなり、結果としてテストが比較的行いやすいためである。そして、ビジネスロジックを備えていることから、非常に重要なオブジェクトでもある。ほかには、協力者オブジェクトの数も多く、コードも複雑ないわゆる「過度に複雑なコード」というものも存在する。これはテストするべきだけれど、テストコードが書かれていないとても危険なコードであり、なるべく保守しやすい「ドメイン・モデル/アルゴリズム」や「コントローラ」に寄せてテストコードを書くことが望ましい。
そして具体的な方法として、システム外依存とビジネスロジックが混ざっているコードをそれぞれ分類して切り離すことでビジネスロジックに対してのテストを書きやすい状態にするHumbleオブジェクトが紹介されていた。
第8章 なぜ、統合(integration)テストを行うのか?
統合テストにおいて、プロセス外依存をモックに検証する場合と、直接プロセス外依存を呼び出してその後の状態を検証する場合が存在する。何を基準として各方法を使い分ければよいかを考えると、各プロセス外依存が「管理下にある依存かどうか」というのが答えとなる。
該当のプロセス外依存がテスト対象のアプリケーションの管理下にある場合、例えばテスト対象のアプリケーション内での各オブジェクトとDBのやりとりはテスト対象のアプリケーションを通じてのみ観察可能であり、外部から内部の振る舞いが見られないということを指す。この場合、プロセス外依存は実装の詳細となるため、モックするのではなく直接呼び出した後の状態を検証するほうが有効なテストとなりえる。モックをすると、
一方、テスト対象のアプリケーションの管理下にない場合、例えば外部のサービスが提供しているAPIを呼び出して何かしらの処理を実行する、という場合は外部から内部の振る舞いを確認することができる。この場合、プロセス外依存はテスト対象のアプリケーションの観察可能な振る舞いとなるため、モックを用いてテストすることが望ましいと言える。
第9章 モックのベストプラクティス
モックは管理下にない依存に対して用いるのが望ましいが、より具体的にいうと、外部のアプリケーションとの境界に近い部分が望ましい。そうすることで、内部のアプリケーションの詳細とモックの結びつきを弱めることができ、結果としてリファクタリングへの耐性と後方互換性を持ったテストコードを作成できる。
スパイは依存先のコードを実際に動かしつつ、呼び出し情報を記録し、検証する際に便利。モックと違い、実メソッドを実行しながら呼び出し時の引数などを追いかけることができるので、モックと必要に応じて使い分けることができる。そして、実際に実行された後の結果をあとから検証できる分、スパイを用いたほうが質の高いテストコードを書きやすい。モックの場合、想定したメソッドが想定した引数、回数で呼び出されることまでは見えてもその結果想定した内容が実行されたかまでは担保できていないからだ。
第10章 データベースに対するテスト
実際のDBを用いたテストは非常に退行に強い耐性を持つものとなる。しかし、実際のDBへの書き込みを行う都合上、ほかのテストケースに影響を与えないよう、単体テストとして各テストケースが隔離された状態を保つために、テストケース実施前にそれまでのテストケースの実行結果をリセットする処理を必ず行うことが推奨されていた。このことより、データベースに対するテストを作成していくにあたって、pytestのfixtureを応用して各テストケースの後片付けや下準備を自動化するのは有効な方法だと学んだ。
第11章 単体テストのアンチパターン
プライベートなメソッドは振る舞いが外部から観察可能なAPIを通じてテストすべき、という話はいろんなところで見るし、実際合理的だなぁと思う一方で、テストしたくなるプライベートなメソッドってあるよなぁ・・・と思っていた。が、そもそもテストしたくなるプライベートなメソッドは抽象化が十分でないことを示唆しており、複雑性(ビジネスロジック)を含む部分をパブリックなメソッドとして切り出して別途テストするのが望ましいという記述を見て腹落ちした。また、あるクラスの一部のメソッドをモック化してテストをするというのも、考え方を変えると、テスト対象のメソッドが単一責任の原則に違反している状態を意味しており、まずモック化したい部分とそうでない複雑な部分を別個のメソッドなどに切り離してテストしやすい状態にする(いわゆるHumbleパターン)が推奨されていた。
テストコード内にプロダクションコード内の知識やロジックを流用することがアンチパターンと示されており、例として足し算の例が挙げられていた。テストの期待値をプロダクションコードのロジックを使って計算するというのは、よくよく考えたら意味ないテストを生み出しかねないとわかるが、意外とうっかりやってしまっていることありそうだなと感じた。テストの期待値をちゃんと固定の値で決め打ちして示すことは、期待値を明確化するとともに、テストケースとプロダクションコードをきちんと切り離すのに重要なのだろう。リファクタリング時に既存のコードを実行した際の値を期待値として利用する、というのも非常に有用な知見だと感じた。
Discussion