見出し画像

「単体テストの考え方/使い方」を読んだ

買ったはいいが、1年ほど寝かしていた本をやっと読み終えました。単体テストの本を読むのは久しぶりでしたが、ソフトウェアアーキテクチャあたりも踏まえ、良いテストについて書かれていて、個人的にはとても納得感のある良い本でした。

個人的な頭の整理と感想なので、図やコードなどはほぼ無いです。

単体テストについて私の思うところ

開発プロジェクトにおいて、自動の単体テストを書くというのは、ある程度認知されてきたかと思いますが、実際には上手く運用できていないケースも多いのではないかと思います。

例えば、単体テストを書き始めたが、機能を追加し、リファクタリングしたりするたびに、テストが壊れてしまい、テストを保守していくこと自体の工数が高くついてしまう。ひとまずは成功するようにテストコードを変更し逃げてしまう。そして遂にはCIでもチェックをしないようにしてしまうなど、負の遺産になってしまうというケースもあるのではないでしょうか。

また、ソフトウェアを継続可能なものにするためのリファクタリングも、単体テストを壊さないために行わない、という判断も実際には発生しているのではないでしょうか。

私もあるプロジェクトで、メンバーにリファクタリングを依頼したら「変更してしまうとテストが失敗してしまいます。そのテストを直すとなるとスケジュールに間に合いません」的なことを言われたのを覚えています。

リファクタリングは、インターフェースを壊さずに、ということはあるかもしれませんが、初手ではそこまで読めないこともあり、実際には引数が追加になる、メソッドが分割される、オブジェクトグラフが変わってしまう、ということはあると思います。それはリファクタリングと言うのか?など一言物申したい方もいるかと思いますが、リファクタリングの定義は傍に置いておいて、プロダクションコードを良くするために行うべき行為を、単体テストが出来ないようにしてしまうというのは、憂慮すべきことかなと思います。

そういったこともあり良い単体テストとは何だろうか?どう作れば良いのだろうか?というのは、多くのプログラマにとっても重要な要素になってきているのではないかなと思っています。

その点について教えてくれる本になっているかなと思います。

カバレッジとテストの質の関係性

本書ではカバレッジとテストの質の関係性について、実際のプロジェクトで発生しがちな状況も含めて、その目的や使い方に関して述べられています。

カバレッジを100%にしたからと言って、本当の意味で網羅的にテストが出来ているのか?正しく検証ができているのか?良いテストが出来ているのか?は分からないため、そういった目的で利用するのには注意が必要だよ、ということを言っています。

カバレッジ自体は、テストが十分に行われていない可能性があることを可視化してくれるツールとして利用するのが望ましく、それ自体を目標とするのはアンチパターンと言っています。

これは完全に同意ができるところになります。あくまで足りてなさそうだからもう少しテストした方がよいかもね、といった程度に留めておくのがよいかなと私も思います。

学派の違いからくるモックの利用

テストの書き方には古典学派とロンドン学派があります。この学派についての話は久々に見ましたが、モックの利用に関する、この本の重要な部分に進んでいくための理解しておくべき点になっていました。

古典学派はテスト駆動開発、ロンドン学派は実践テスト駆動開発ということで、初めてこれらの書籍を読んだ時は、あまり気にしていなかったこと、また実践テスト駆動開発を読んだ当時の私には、その内容は難しくほぼ理解できずに、とりあえず読んだ、ぐらいになっていたため、改めて時間を作って読み直しておこうかなと思いました。

ちなみに、ピアソンから出ているテスト駆動開発入門はAmazonの中古で250円だそうです。私が買った時は普通のお値段だった気がしますが、絶版になってしまっているのにこの値段というのは、和田さんが新たに翻訳してくれたからでしょうか。ありがたいことです。

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

この本では良いテストを構成する4本の柱として以下を定義しています。

  • 退行(regression)に対する保護

  • リファクタリングへの耐性

  • 迅速なフィードバック

  • 保守のしやすさ

退行(regression)に対する保護は、機能追加など何かしらの変更をした場合に、混入してしまったバグをテストで検出できるか?ということです。

リファクタリングへの耐性は、既存のテストが失敗することなく、リファクタリングが出来るかどうか?ということです。この中で出てくる偽陽性というのは本書を通じて理解しておくべき言葉となっています。

迅速なフィードバックとは、テストが早く終わり、すぐに結果を受け取れるか?ということです。

保守のしやすさとは、テストケースの理解のしやすさ、テストを行うことの難易度のことです。

それぞれについて、大事だと理解できるかなと思いますが、それをどう満たすのか?どういったバランスを目指すのが良いのか?ということが、本書を通じて述べられています。

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

これはリファクタリングへの耐性に大きく関わることで、実装の詳細をテストしてしまうと、偽陽性が発生すると言っています。

観測可能な振る舞いとは、以下の2点となり、当て嵌まらないものが実装の詳細になると定義されています。

  • クライアントが目標を達成するために使う公開された操作

  • クライアントが目標を達成するために使う公開された状態

例えば、名前を変更するというユーザーが達成したい目標があった場合に、それを実現するための公開されたAPIが観測可能な振る舞いで、それ以外をサポートするコードは、実装の詳細となるため公開してはいけない、ということです。

この観測可能な振る舞いは、どの目線で見るかによって変わります。ユーザーや外部システムから見た場合、コントローラークラスから見た場合、ドメインモデルクラスから見た場合など、それぞれの立場から見て、どのAPIが名前を変更するという目標に対しての観測可能な振る舞いなのか、ということです。

観測可能な振る舞いをテストしていた場合は、当然それが実現したいことのため、リファクタリングで、内部のメソッドの分割粒度が変わったり、メソッド名が変わったとしても壊れることはないはずです。

実装の詳細はテストしていた場合は、内部の実装を変えただけなのに、テストが失敗し出すことになります。これではテストの保守が大変になってしまいます。そうならないようにどうするのが良いのか?ということが、本書の全体にわたって述べられています。

多分、実際に実装し出すと、publicメソッドがとっ散らかっていくので、常に意識しておきたいポイントだなと感じました。

単体テストの3つの手法

この本では検証の方法として、以下の3つを紹介しています。

  • 出力値ベーステスト

  • 状態ベーステスト

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

そして、先ほどの良い単体テストを構成する4本の柱とどう関係するのか?ということが書かれています。

結論だけ書くと、リファクタリングの耐性保守のしやすさについては、各手法で異なると述べられています。対して退行に対する保護迅速なフィードバック各手法で異なることはないと述べられています。

また、単体テストにおいては、出力値ベーステストを可能な限り使った方がよく、次に状態ベーステスト、使わない方が良いものがコミュニケーションベーステストとなっています。

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

関数型アーキテクチャとヘキサゴナルアーキテクチャについて、その類似点と相違点が述べられています。どちらも2つのレイヤーがあり、外側のレイヤーと内側のレイヤーがある形です。

ヘキサゴナルアーキテクチャは、ports and adaptersとも呼ばれています。外側がアダプターで、内側がアプリケーション(ドメイン層)です。ポートはクライアントからのリクエストを受け取り、アダプターに流すという形になっています。

関数型アーキテクチャは私は初見でした。内側は関数的核(functional core)で、外側が可変殻(mutable shell)です。

両方ともに関心の分離が基盤となっていて、依存の流れが一方向になっているということです。異なる点は、全ての副作用を関数的核の外に追い出すというところになります。処理としては関数的核の前後に副作用の伴う処理は実行するとなっています。ヘキサゴナルアーキテクチャを関数型的に考えるとこうなるというところで、この辺りも面白いところでした。

本書では関数型ではなく、クラスをテストしやすくする形で進めてます。そのため出力値テストで出てくるプロダクションコードは、コンストラクタから受けとったオブジェクトを使いながら、クラスを使った出力値テストの現実的な方法を提示しています。また、実際にはそう簡単には出来ないこと、その課題に対してどういった方法があり、どんなトレードオフがあるのか?などが解説されています。

ここら辺は、実際の開発では難しいことの方が多いと思うので、方法やトレードオフについては、1つの指針としては有益な説明がされているかなと思います。

4種類のプロダクトコード

この本では、コードを以下の4種類に分類しています。

  • ドメイン・モデル/アルゴリズム

  • 取るに足らないコード

  • コントローラ

  • 過度に複雑なコード

ドメイン・モデル/アルゴリズムは、ドメインに関連するビジネスロジックを扱っているコード、または複雑なアルゴリズムなど、テストをする価値が高いコードのことです。そして本書における単体テストの対象コードになります。

取るに足らないコードは、GetterやSetterなどロジックを含まないテストする価値のないコードになります。本書では述べられていませんが、GetterやSetterにロジックが入ることはあると思いますので、その場合は単体テスト対象になると思います。

コントローラは、複数のコンポーネント(ドメインモデルなどを扱うクラスや、外部のアプリケーションを呼び出すクラスなど)を扱い、それらが適切に連携できるように調整を行うコードです。複雑なことはせずに、コンポーネントを呼び出し、結果を受け取り処理を進めるだけのコードです。そして本書における統合テストの対象コードになります。

過度に複雑なコードは、コードの複雑さ、ドメインにおける重要さも高く、協力者オブジェクトも多く持つテストすることが困難なコードです。本書では、このコードをドメイン・モデル/アルゴリズムコントローラに分割して、テストが行える状態にする方法が書かれています。

モックの利用

本書ではロンドン学派のように、内部に持つ別クラスをすべてモックにすることは、リファクタリングへの耐性を損ねるためすべきではないと述べています。モックにするのは、管理下にないプロセス外依存のみと述べています。

管理下にないプロセス外依存とは、自システム以外から利用されるかモノか?見えるモノか?といった判断軸になります。例えばメッセージ送信に関しては、送る相手がいて、そこにはこういう仕様でメッセージを送るべきという後方互換性も含め維持していくための厳密な守るべき契約があります。

対して、自システムしか利用しないデータベースは、自システムを通してしかアクセスがされないため、どのようにリファクタリングしても、最終的な結果さえあっていれば問題ないことになります。そのため、プロセス外依存だが管理下にあることになり、モックにすべきではない、ということになります。もちろん別システムとデータベースを共有するケースもあると思うので、その場合は管理下にないプロセス依存になるため、モックで検証することになります。

また、モックにすべき対象もアプリケーション境界に位置するインターフェースとなります。アプリケーション境界とは、ヘキサゴナルアーキテクチャでいうと、アダプタが配置される層の外側の線になります。その線に一番近い部分のインターフェースということになります。そうする事でテスト実行時に最も多くのコードを動かすことができ、退行に対する保護が高まると述べています。

統合テスト

本書では単体テスト以外で、E2Eではないテストを統合テストと定義しています。

統合(結合)テストというと、複数の機能や画面に対して、機能間、画面間、データ連携などのテストを行う、という目的で言われていることも多いですが、本書の範囲としては、結果としてプロセス外依存を扱うクラスを利用するコントローラーのテストになっています。プロセス外依存は、管理下にあるモノ、ないモノの両方となります。

そして、1つの最長のハッピーパス(正常系)と単体テストでは検証できなかった異常ケースをテストすべきとなっています。統合テストでは実際のデータベースを扱ったりなど、単体テストに比べてテストの実行時間が長くなるため、迅速なフィードバックが損なわれることに起因しており、単体テストよりもテストケースを少なくし、テスト全体の実行時間を少なくするためという目的があります。

これは、本書でも述べられていますが、テストピラミッドという自動テストケース数の望ましい比率をピラミッド型に視覚化したものからきています。

いずれにしても、単体テストで多くのテストを行い、そこでは出来なかったプロセシ外依存をテストするのが、統合テストになります。

インタフェースについて

この本では、インタフェースについて以下の2点の場合に必要になると言っています。

  • ある抽象に対して実装が2つ以上になる場合

  • 管理外のプロセス外依存をテストするためモックが必要になる場合

これは賛否両論が出そうかなと思いますが、私としては納得感がありました。

データベースを扱うクラス(リポジトリなど)の場合に、インタフェースを用意するというのは、無条件で行われている場合がありますが、これは結構疑問に思っている人もいるのかなと思います。

確かにインタフェースを用意し、DIをしたり、ファクトリークラスでインスタンスを生成することで、利用側は具象クラスを知らなくてよくなり、インターフェースにだけ依存することになります。その結果、別のクラスに差し替えができる。など言われていたりします。

これって、必要になったタイミングでインタフェースを用意するのと、具象クラスが1つしかないタイミングからインタフェースを用意するのとで、何が違うのでしょうか?つまりYAGNIではないのでしょうか?

実際には、メソッドのシグネチャが変わらなければ、必要になったタイミングで、インターフェースを用意するということでも、目的は満たせるのではないでしょうか?また仮にシグネチャが変わるのであれば、前もって用意していたインタフェースのシグネチャも変わってしまうのではないでしょうか?

インタフェースでプログラミングとは、実装の詳細を意識せずに、インタフェースのみを意識してプログラミングをするということなはずです。

そのため、言語の機能としてのインタフェースを利用していても、実装の詳細を考慮した上での呼び出しなどを行なっていれば、それは結合しているのと変わらなくなってしまいます。そして、それがもっとも顕著に現れるのがテストコードかなと思います。

そう言ったこと、それに対する考え方や方法論が、この本には書かれていました。私としては今のところは、腑に落ちた気がしています。

まとめ

実際には、この本に書かれていることのように、上手くいかないことも多い気はしますが、1つの指針としては参考になる考え方や方法論かなと思います。

また、カバレッジや単体テスト、統合テスト、E2Eテスト(テストピラミッド)の関係性など、コード以外の箇所については、プロジェクトマネージャーなどプロジェクトを運営する方、プロダクトマネージャなどプロダクトをより良いものにする方にも読んでおいて損のない内容が書かれている本かなと思います。


この記事が参加している募集

この記事が気に入ったらサポートをしてみませんか?