幡ヶ谷亭直吉ブログ

娘のここねと格闘するエンジニア。

『単体テストの考え方/使い方』を読んで ~ 単体テストも振る舞いを捉えるという痛感

読書メモ。2025年29冊目。
単体テストの考え方/使い方』を読んでの感想となります。(2025/4/19記載)

本の概要

質の高いテストを行い、ソフトウェアに価値をもたらそう!
単体(unit)テストの原則・実践とそのパターン ― プロジェクトの持続可能な成長を実現するための戦略について解説。

優れたテストを実践すれば、ソフトウェアの品質改善とプロジェクトの成長に役立ちます。逆に間違ったテストを行えば、コードを壊し、バグを増やし、時間とコストだけが増えていきます。生産性とソフトウェアの品質を高めるため、優れた"単体テスト"の方法を学ぶことは、多くの開発者とソフトウェア・プロジェクトのために必須といえるでしょう。

本書“単体テストの考え方/使い方”では、単体テストと統合テストの定義を明確にします。そして、どのようなテストに価値があるのかを学び、どのテストをリファクタリング、もしくは削除するのか、ということについて考え、そのことがプロジェクトの成長にどう繋がるのかを見ていきます。
C#のコード例で解説しますが、どの言語にも適用できる内容です。

Manning Publishing: Unit Testing Principles, Practices, and Patterns の翻訳書。

引用:

book.mynavi.jp

動機

  • 当書に対する高評価をいろいろなところで見かけてずっと気になっていた。
  • とはいえ、エンジニア歴18年にして単体テストを学ぶのは、と思い躊躇していた。
  • JaSST Tokyoでいろいろお話を聞いて改めてテストを学びなおしたくなりブースで購入!!

感想

なんでもっと早く読まなかったのかと反省しました。
t_wadaさんの以下のお話は前から知っていたけど、ここまで自分の認識を改める必要があったとは。

speakerdeck.com

 

エンジニアとして最も多くユニットテストを書いていたのは10年ほど前だと思いますが、その頃を思い出し、反省することが多くありました。
当時私はユニットテストは実装の詳細を確かめるものであり、実装に手を入れればユニットテストもメンテナンスすべきものと考えていました。

本書ではユニットテスト単体テストと統合テストと切り分けた上で、そのうえで単体テストも実装の詳細ではなく、振る舞いを確認すべきであるとと述べられています。
実装の詳細を確認するためのテストコードは、振る舞いが変わらなくてもエラーと判断してしまい(偽陽性)、リファクタリングへの耐性が弱くなり、結果保守コストが高くなると説明されています。
私は実装が変更されたことを気付けるために、実装の詳細を確認するためのテストコードを書いていたので、ハンマーで頭をたたかれた感じがしました。
この本で記載されている方針で言えば、消した方が良いテストがたくさんあったと思っています。

私がエンジニアになりたての頃は単体テストも手でやるものでした。
プログラムコード上の分岐を網羅するための手動試験のための仕様書を書き、それを手動でテストしていました。
その延長線上として、ユニットテストは手でやっていたものをコードで実現すると理解していた気がしています。
結果、プログラムコードをなぞる形での実装が、知らぬ間に「単体テストの目的」そのものだと誤って捉えてしまっていたように思います。

本書では、単体テストをまるっきりのブラックボックスとしてやるべきとは書いていません。
そうではなく、ホワイトボックスとしてロジックを明らかにしたうえで、振る舞いにフォーカスすべきであると書いています。

今後はこの方針を念頭に単体テストに取り組んでみたいと考えています。
正直、今までのやり方が根が深いので、学んだことを体験を通して理解し、自分のものにしたいと考えています。
まだ、単体テストで振る舞いを確認していくことが、充分に理解できていない気がしています。

改めて、自分に慢心があったことを痛感させられる読書体験でした。
非常に良い本だったので、所属する開発チームにも推薦しています。

とても大きな学びになりました。読んでよかったです。

忘れたくないメモ

■なぜ、単体テストを行うのか

ソフトウェア開発プロジェクトの成長を持続可能なものにする

■コードは負債。質の良いテスト・ケースだけを集める

コードは資産ではなく負債。コードは最小限にすべき。テスト・コードもまた、他のコードと同じように、バグに対して脆弱であり、保守を必要とするものなのです

■網羅率ではテストの質の良さは証明できない

網確率はテスト・スイートの質が悪いことを示せても、テスト・スイートの質が良いことを証明することはできない

網羅率はプロダクション・コードが実行されたことを示すだけで、実行結果が確認されたことを保証するわけではない

網羅率を最大限に活用するためには、網羅率の数値を目標とするような使い方をせず、テストが十分に行われていないことを示すものとして見なくてはなりません。

■優れたテスト・スイートの特徴

優れたテスト・スイートには次の特徴があります。
・テストすることが開発サイクルの中に組み込まれている。
・コードベースの特に重要な部分のみがテスト対象となっている。
・最小限の保守コストで最大限の価値を生み出すようになっている。

単体テストの定義

次の3つの性質をすべて備えるものが単体テストとなるのです。
・「単体(unit)」と呼ばれる少量のコードを検証する
・実行時間が短い
・隔離された状態で実行される

■古典学派における単体テストの優位性

古典学派における単体テストの隔離とは、テスト対象となるクラスを共有依存から隔離することを意味する。そのため、プライベート依存であれば、テスト・ダブルに置き換えずにそのまま使っても問題はない

私個人は古典学派のスタイルのほうを好んでいます。その理由は、古典学派のほうがより良質な単体テストを作成でき、単体テストの究極的な目標である、プロジェクトの持続的な成長を促す、ということを達成するのに向いているからです。このようになるのは、ロンドン学派のスタイルで作成するモックを用いた単体テストは古典学派のスタイルで作成する単体テストよりも壊れやすくなるからです

コードの粒度を細かくしようとすることは単体テストにおいてあまり有用ではありません。少なくとも、1単位の振る舞いが検証されてさえいれば、それは良い単体テストとなるのです。逆に、検証の対象となるものがそのことを満たせていなければ、その単体テストが何を検証しようとしているのかが曖昧になるため、質の悪い単体テストが作成されます

各テスト・ケースですべきことは、そのテストに関わる人たちにテスト対象のコードが解決しようとしている物語(story)を伝えることなのです。そして、その物語を伝えるためには凝集度 (cohesion)を高め、非開発者でも理解できるようにすることが必要

複雑な依存関係を持つテスト対象システムを古典学派のスタイルでテストするのであれば、テスト対象システムを適切に機能させる目的のためだけにすべての依存(ただし、共有依存は除く)を用意して複雑な依存関係を作り上げなくてはなりません。そして、それを行うことはかなりの手間がかかります

ほとんどの場合、このような複雑な依存関係が必要となってしまうのは間違った設計が行われたことが原因

■シナリオが正しいことの証明

各テスト・ケースはプロダクション・コードが解決しようとしている物語(story) について語るべき、ということが強調されます。ここで言う語るべき物語とは、問題領域に関する個別で不可分なシナリオ (もしくは事実)のことです。そして、その物語を語るテスト・ケースが成功で終わることは、そのシナリオ (もしくは事実)が正しい。ということの証明になります。

単体テストですべきことは、プロダクション・コードが何をするのかを単に列挙することではなく、アプリケーションの振る舞いについてより高いレベルで描写することだからです。そして、理想なのは、単体テストで表現していることが開発者だけでなく非開発者にも伝わることです。

単体テストの単位

単体テストで検証する単体とは1単位の振る舞いのことであり、1単位のコード、つまり、クラスのことではない。そして、この1単位の振る舞いは複数のクラスにまたがることもあります。そのため、実際に呼び出されるクラスの中にはテスト・クラス名に使われていないクラスも含まれることがあります。しかしながら、対象の振る舞いを検証するためには、その振るいを呼び出すのに起点となるクラスが必要となります。そこで、(クラス名) Testsのフォーマットを用いることで、1単位の振る舞いを検証する際に呼び出すAPIを持つものとして対象のクラスを識別できるようにしているのです。

■良い単体テストの4本の柱

良い単体テストを構成するものとして、次の4本の柱があります。
・退行(regression) に対する保護
リファクタリングへの耐性
・迅速なフィードバック
・保守のしやすさ

偽陽性

本来は正当な振る舞いであるのにもかかわらず、テストが頻繁に失敗していると、テストに対する信頼が徐々に失われていく。その結果、テストのことを信頼できるセーフティ・ネットとしては見られなくなり、テストを行う意味が薄れてしまう。そうなるとテストによって進行を防ぐことよりも、コードの変更をできるだけ行わないことが退行を防ぐ最善の手段として考えられるようになり、リファクタリングが敬遠されるようになる。

テスト・コードが検証する対象を観察可能な振る舞いとし、その結果を得るための細かい手順である実装の詳細には目を向けないようにするのです。そのためには、テスト対象のコードを呼び出す側の視点で検証し、その呼び出す側にとって意味のある実行結果のみを確認し、その他のことに関しては何も検証しないようにしなくてはなりません(このことは第5章でさらに詳しく見ていきます)

最善の単体テストは保守のしやすさとリファクタリングへの耐性を最大限に備えたものである。そのため、これら2つの性質は常に最大限に備えられるようにしなくてはならない。一方、進行に対する保護と迅速なフィードバックの2本の柱のどちらを優先するのか、というバランスを調整することになる

ブラックボックステスト

すべての種類のテスト(単体テスト、統合テスト、E2Eテスト) において、テスト対象のコードをブラック・ボックスとして見るようにし、問題領域にとって意味のある振る舞いのみを検証の対象とする

作成したテスト・ケースからビジネス要求を導き出せないのであればそのテスト・ケースはすぐに壊れてしまいます。そのため、そのようなテスト・ケースをそのままテスト・スイートの中に含めてはならず、作り直すか破棄するかのどちらかを選択しなくてはなりません。ただし、そのテスト・ケースがアルゴリズムにおける複雑さを備えたユーティリティ・コードに対するものであれば、そのテスト・ケースを例外的にテスト・スイートに含めても問題ありません

■コマンド・クエリ分離

モックとスタブの考え方はコマンド・クエリ分離 (Command Query Separation: CQS)の原則との関連性があります。コマンド・クエリ分離の原則では、次の図53で示すように、すべてのメソッドはコマンドかクエリのどちらかになるべきであり、両方の性質を持つべきではない、ということを提唱しています。ここで言うコマンドとは、どのような値も返すことはなく(つまり、戻り値がなく)、副作用を起こすメソッドのことを指します。

このことを踏まえてテスト・ダブルについて考えると、コマンドの代わりとして使われるテスト・ダブルがモックであり、クエリの代わりとして使われるテスト・ダブルがスタブとなります。

ドメイン層とアプリケーション・サービス層

ドメイン層はアプリケーションのドメイン知識(how-to)を集めたものとして見ることができるのに対し、アプリケーション・サービス層はビジネスにおけるユース・ケース(what-to)を集めたものとして見ることができる。

単体テストを行う価値があるコード

単体テストを行う価値がもっとも高いプロダクション・コードは複雑なコード、もしくは、ドメインにおける重要性が高いコードです。テストをする価値を高めるためにドメイン層のコードを複雑にする必要はない。複雑なコードが必ずしもドメインにおいて重要になるわけではありません。

コントローラがいかに多くのオブジェクトを指揮していても、そのコントローラは複雑さを持ってはならない。一方、ドメイン・クラスはその逆となるようにしなくてはならない

単体テストと統合テスト

一般的に、単体テストはビジネス・シナリオにおける異常ケース (edge case) をできるだけ多く検証するのに対し、統合テストは1件のハッピー・パス (happy path)と単体テストでは検証できないすべての異常ケースを検証することが適切だと考えられています。

統合テストはコントローラを検証し、単体テストドメイン・モデルやアルゴリズムを検証する

■質の悪いテスト・ケース

質の悪いテスト・ケースを作成するくらいなら、テスト・ケースを作成しないほうがまだましです。ここで言う質の悪いテスト・ケースとは、明確な価値をもたらさないテスト・ケースのことを指します。