はじめに
TIG EXU真野です。
積読を消化しようというテーマの、読書感想文連載 の1冊目は、単体テストの考え方/使い方 です。
書籍の基礎情報です
- 2022年12月28日発売
- Unit Testing Principles, Practices, and Patterns の翻訳書。原著は2020年1月14日に発売
- テーマ
- 質の高いテストを行い、ソフトウェアに価値をもたらそう! 単体(unit)テストの原則・実践とそのパターン
- プロジェクトの持続可能な成長を実現するための戦略
- 単体テストの原則・実践とそのパターン
- コード例は C# であるものの、どの言語でも適用できる汎用的な内容とのこと
- 中を見ると、微妙にC#特有ぽいところに1箇所悩みましたが、それ以外はその通り
- 翻訳者の須田さんは、他にもセキュア・バイ・デザイン: 安全なソフトウェア設計 やOAuth徹底入門 セキュアな認可システムを適用するための原則、RxJavaリアクティブプログラミング の翻訳もされており、知らずに3冊まで私の本棚に揃ってました
- どれもキレイに訳されており、日本語として自然だった記憶があります
- 表紙は北欧ぽい服装をした女性については、詳細が末ページに記載がありますのでお楽しみに
私が観測している範囲では非常に評判がよく、ソフトウェア設計品質全般に役立つといった声も聞こえてきます。実際に、単体テストのTips的な書き方だけではなく、どういった単体テストが長期間有効なテストとなりえるのか、といったことを説明されています。 持続可能性 という用語が入っているのも最近のトレンドがあるなと個人的に感じています。
個人的に書籍で学びたいと思ったのは以下のポイントでした。
- テストコードのレビューはアプリケーションコードより難しいと感じる点、どうにかならないのか
- テストデータの正しさの確認が難しい
- テスト密度が増えるとどんどん、テスト実行時間が増える。効果的な運用とは
内部は4つの部で構成されているため、それぞれごとに書簡を述べていくスタイルとします。
第1部 単体(unit)テストとは
3つの章で構成されています
- 1 なぜ、単体(unit)テストを行うのか
- 2 単体テストとはなにか?
- 3 単体テストの構造解析
ここで出てくる書籍に一貫して伝えられるメッセージである、「テストの労力を抑えつつ、テストから最大限の価値を引き出すこと」とはどういうことかは、慣れた開発者ならだれしもが意識することだなと思いますが、うまく言語化されて凄いと感じました。
個人的にはC#の文化圏と、 Goの文化圏の違いを大きく感じたところで、例えばテスト対象メソッド(Method Under Test: MUT)の名前についてや、AAAパターン(Arrange, Act, Assert)については、TableDrivenTests のGoコミュニティ(?)の刷り込みがあって、少しだけギャップを感じました。
もちろん、書籍の考え方自体はかなり有用で、ギャップは細かい部分です。例えば、メソッド名についてはTableDrivenTestsのケース名に値するねとか、AAAパターンについては特に意識しなくてもそうなりそうとか、11章のアンチパターンに記載されていた、ドメイン知識のテストコードへの流出についても、ほぼ発生しないだろうなといった具合です。
以下、個別トピックで気になった部分です。
- 良いテストについて、定量的にに見る方法としてカバレッジ・分岐網羅などの話もでてきます
- これをKPIにすると間違った運用になるので、メトリクスとして使おうねといった趣旨には納得感があります
- 実践的には、どれくらいの数値がベターなのか、指針となる数値があると良いなと感じました(※ただし、言い出すとキリがなく書籍のスコープ外という気もします)
- これをKPIにすると間違った運用になるので、メトリクスとして使おうねといった趣旨には納得感があります
- モックテストの流派(古典学派、ロンドン学派)の違いもそれなりのページを取って説明しています
- 正直、興味がなかったのですが、それぞれの違いを理解し使い分けようという感じではなく、著者が最初から主張している哲学でいくのであれば、古典学派の考え方を採用すべき、とハッキリ推奨していたのが印象的でした。納得感がありました。やっと流派の違いを理解できた気がします
- 単体テストで複数のシナリオをつなげることのメリットとでメリット
- テストの実行速度は早くなるものの基本NGということと、これは統合テストでやるべきだよねという話は同意しかなかったです
- なんとなく、TableDrivenTestsで前のテストケースに依存したケースを書いてしまうアンチパターンに似ているなと感じました
- テスト対象メソッド名に should be (~であるべき)は入れない用にしようといった話があり、従おうと思いました
- C#のコンストラクタについて
- 以下のようにコンストラクタや
setter
でFormatName()
のようにロジックを差し込むと、location.Name = newName
みたいにフィールドに直接代入してもFormatName()
が呼ばれるようで、少し混乱しました。C# わかっていないので、勘違いしたらすいません - 参考: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/expression-bodied-members#constructors
- 以下のようにコンストラクタや
public class Location |
第2部 単体テストとその価値
4つの章で構成されています。
- 4 良い単体テストを構成する4本の柱
- 5 モックの利用とテストの壊れやすさ
- 6 単体テストの3つの手法
- 7 単体テストの価値を高めるリファクタ林
2部はかなり楽しい内容でした。良い単体テストの観点は、個人的には想定した仕様を満たすか検証を早くできる・改修で壊れたときに検知できるなどと思っていましたが、それらをより明示的に言語化して4つに整理したなと感じます。それぞれの観点について、SN比での説明や、ホワイトボックステスト/ブラックボックステストのどちらをテストケース洗い出しに活用し、どちらを分析に利用すべきといった話はとても納得感があります。
ここで一番わたし個人の経験と紐づいてグっと来たのは、観測可能な振る舞いをテストすべきで、実装詳細をテストすると壊れやすい(保守性が下がる)という点です。例えばO/Rマッパーの生成するSQLクエリの文字列を検証すると、壊れやすいテストになるなといった具合で、過去を思い出してうなりました(クエリ系のテストについては3部でも説明があります)。How, Whatのどちらに絞ってテストを書くべきか、読み進めると納得感が深まると思います。
以下、個別トピックで気になった部分です。
- 良い単体テストの4本柱については、改めてこの領域に絞って深く考えたことが無かったので良い振り返りになりました
- 「テストを書くことは当たり前になっている人で、自動テストの価値も理解している」が、「どのようなテストであるべきか、悩んでいる人がいると、指針になる」と書いてあって、その通りな内容です!
- 4つのパラメータがあると、当然出てくる(考えたい)のがトレードオフ構造ですよね
- 例えば、DBまで接続したE2Eテストだと、間違いに気が付きやすいが、テスト実行速度が犠牲になりフィードバック間隔が遅れるといった話
- 面白いのは、全てトレードオフではなく、ある観点は捨てることができないので、実質制御できるのは2軸になるという点です。トレードオフ構造が好きな界隈の人には、共通認識としてあると良いかもと感じます
- トレードオフ構造については、明らかに「CAP定理」を意識しています(本書にも言及があります)
- CAP定理はデータ指向アプリケーションデザイン 本を見れば、もはやそれ自体は過去の存在になったと思いましたが、そのフレームに関して業界に与えたインパクトの大きさを再認識しました
- トレードオフ構造ですが、 完全性・純粋正・性能のトリレンマがあるという、kawashimaさんの強いて言えば「集約どう実装するのかな、を考える」会勉強会を思い出させます。繋がっていると感じます
- ※詳しくは、 syobochimさんの参加レポートを参考に
- (物議ある)モックについて書かれているのも、2部からです
- テストダブルの分類(モックかスタブかなど)は分かりやすく、DBはこっちで外部メールサービスがこっちなら、マイクロサービスのWeb APIはどっちなんだ。DBにそのマイクロサービス経由でアクセスしたら話が変わらないか? と一瞬思ったものの、すぐ後ろの文章で考えの軸があり、すぐ疑問が消失したのが良い文章の流れだなと感じた記憶です
- 少し面白いのは、CQS(コマンド・クエリ分離の原則)について言及があり、テスト対象メソッドについても、コマンド・クエリのどちらかであるべき。それぞれどちらかの性質を持つのであればモック、もう片方であればスタブとして作成すべきといった話です
- モックとスタブすら区別して考えていなかった自分には斬新な整理の話に感じました
- あと、書籍では「コマンドの戻り値は無い」と話していますが、実際は生成したリソースのIDや、errorは返すべきなので適度に読み替えが必要そうです
- (後者はC# だとExceptionで広域脱出できるので、 error を毎回返すGoの流儀が特殊なだけではあります)
- 関数型アーキテクチャ
- モデルの完全性を取るか、純粋性を取るかの話に近いと感じました
- 前の感想と重複しますが、アーキ部:強いて言えば「集約どう実装するのかな、を考える」会に参加してきた! を併せて読むと味わい深かったです
- 万能なアーキテクチャなんて無いんだという話と、伝統的なアーキテクチャがうまく機能する場面も多いといった話。結局そうなんだよなぁ..
- フレームワークと紐づく場合のテストについて
- テストしにくいから、という前提がある議論に思えました
- 他のフレームワークへの移植性という話はなかったと思うので、逆にフレームワーク側がテストヘルパーを提供していて、それがマッチするなら依存しても良いのでは? という話にもなりそうと感じました
第3部 統合(integration)テスト
3つの章で構成されています。
- 8 なぜ、統合(integration)テストを行うのか
- 9 モックのベスト・プラクティス
- 10 データベースに対するテスト
先程までは単体テストでしたが、統合テストについての部です(違いは書籍内でしっかり説明されています)。私の所属するチームではDB接続までするテストを単体テストと定義しているので、名前がミスマッチでややこしい! となりました。
以下、個別トピックで気になった部分です。
- テストの効率を上げるために、アプリケーションコード側についても言及があって楽しかったです
- 抽象化のためのインタフェースは具体が1つだったら意味がないのでやらない。YAGNI原則違反になる
- 心から同意しました
- 個人的にはテスト用のモックを作るためのインタフェースもなるべく避けたく、Goであれば net/http/httptest のテストサーバ側を立てて管理する側に寄せたいと思っています。このへんの議論はもっと深めていきたいと思っています
- レイヤーを減らそうという話
- 間接参照(indirection)の層を追加しようとする人が少なくないが、コードベースのどこに何があるか把握することが難しいのでやめよう
- これも同意です。澁川さんの データベースと向き合う決意 にも中間層を壊すムーブメントがあると言われていますが、慣習でレイヤーを設けるのではなく、最適な設計の落とし所を見つけるようにすべきだと再認識しました。
- 抽象化のためのインタフェースは具体が1つだったら意味がないのでやらない。YAGNI原則違反になる
- データベースを用いるテストの並列実行が難しいという話(テストケースごとに依存するテーブル、レコードが重複すると影響を受けるため)
- とても分かる
- Dockerコンテナをテストケース(テスト関数)ごとに、インスタンスを分けて利用することは推奨しない(生成・破棄など考慮すべきことが多い)とあったが、これはory/dockertest でどれくらい緩和されるのかは、どこかで検証してみたいと思いました
- データの後始末について
- go-exceltesting といったツールがあり、逆にこういうのを育てて行きたいと思います
- このあたりの界隈は、日本の業界のほうがナレッジが溜まっていそうと感じます
- go-exceltesting といったツールがあり、逆にこういうのを育てて行きたいと思います
- OR Mapper
- 「クエリの単体テストは効果が薄いのでやらないべき(実装の詳細になる)。むしろ統合テストのシナリオの一部にすべき」とあり、納得感がありました
- 単体テストでOR Mapper が生成したSQLクエリをテストすることも可能でしょうが、それがどのくらい役立つかは限定的だと理解しています
- 「クエリの単体テストは効果が薄いのでやらないべき(実装の詳細になる)。むしろ統合テストのシナリオの一部にすべき」とあり、納得感がありました
第4部 単体テストのアンチ・パターン
4部は1つだけの章で構成されています。
- 11 単体テストのアンチ・パターン
主要なアンチパターン(慣れればその通りだけど、テストコードを書きはじめのときは悩むことが多い、というか私は悩んだ)ことがまとまっています。
1点、気になったところがあります。
- テストで用いる現在時刻について(実行するたびに動的に検証項目の値が変わるのをどう制御してテストするか)
- Goで私がテストでよく用いる、 Songmu/flextime は書籍に記載があった、環境コンテキスト型だと思います。これはプロダクションコードを汚す(テストのために書き換える)ので、推奨しないと書いてありました
- この意見には納得できますが、flextimeは許容しても良いんじゃないかと思っていて、自分の中で要言語化だと感じました。今は3つの理由が思い付けます。もっとあるかもしれません
- インタフェースが標準の time.Time と同じだから
- テスト実行フラグのON/OFFで挙動を変えるものでも無いから
- テストコードのために書き換えが発生するが、際限無く書き換えを促す変更ではなく、勘違いも発生しにくい
- この意見には納得できますが、flextimeは許容しても良いんじゃないかと思っていて、自分の中で要言語化だと感じました。今は3つの理由が思い付けます。もっとあるかもしれません
- Goで私がテストでよく用いる、 Songmu/flextime は書籍に記載があった、環境コンテキスト型だと思います。これはプロダクションコードを汚す(テストのために書き換える)ので、推奨しないと書いてありました
全体を通して
- 訳が良い(日本語として読みやすい)
- 個人的には、リグレッション検知→デグレ検知、統合テスト→結合テスト と呼ぶことが多かったのですが、一般的には書籍の表記が正しいと感じます
- 各部にある「まとめ」が非常に丁寧でまとまっているなと感じました
- 全体で400pと、それなりの分量があるため、まとめを読んで納得できないところがあれば立ち戻れるので、良い構成だと感じています
- 単体テスト~統合テストまでの整理が素晴らしい
- 何を重視すべきか、自分の中で考えの指針を作れたのは感謝しかない
- 効率的なテストデータの作り方、レビューの仕方といったところは物足りなかった
- データパターンの網羅などの観点はあまり無く(これはこれで深いので、別分野なんだろうなとは思っています)
まとめ
単体テストの考え方/使い方 についての感想文でした。非常に興味深い内容で、良いテストを作るという観点から、アプリケーションコードの設計レベルまで踏み込んで考えることもできると思います。
次は原木さんの実践Redis入門 技術の仕組みから現場の活用までです。