※本記事における単体テストとは、手動でおこなう単体テストではなく、JUnitやJest等で自動化された単体テストのことを指しています。
参考文献
単体テストの考え方/使い方
この本を読もうと思ったきっかけ
参画しているプロジェクトで自動テストを書いてるが、自動テストの価値自体は理解しつつ(しているつもり)も、かかる労力のわりに本当に利益を享受できているのか?テストのためのテストになっていないか?と疑問に感じる場面が多かった。
もっと効果的な、質の高いテストの書き方を学びたく、単体テストで有名な本書を手に取った。
なぜ単体テストをおこなうのか?
結論:ソフトウェア開発プロジェクトの成長を持続可能なものにするため。
質の高い自動テストがあると、コードに変更を加えても既存の機能にバグが発生していないことを確認、保証できる。そのため、機能追加やリファクタリングを安心して行え、開発スピードを保つことができる。
単体テストの「質」が重要
近年は単体テストが書かれるのが当たり前になってきて、要否を議論されることさえなくなってきた。
しかし、単体テストの「質」について議論されることは少ない。だが、作成されたテストの質が悪ければ、テストを全くしない場合と同じ結果になる。
なぜならテストコードもプロダクションコード(=実装の方のコード)と同じく、保守コスト、維持コストのかかる負債だからである。
可読性の低いテストコードや、プロダクションコードを少し変更しただけで修正しなければいけないテストコード、作成の労力の割に得られる価値が少ないテストコード等は、結局大きな技術的負債となる。なので単体テストの「質」が非常に重要。
質の高い単体テストの特徴として以下があげられる。
- コードベースの特に重要な部分(ドメインロジック)のみがテスト対象となっている。
- 費やした時間に対して価値が最も効果的に返ってくるのはビジネスロジックを含むドメインモデルのコード
- 最小限の保守コストで最大限の価値を生み出すようになっている。
これらをより具体的に落とし込んだ指標として、次の4本の柱がある。
良い単体テストを構成する4本の柱
良い単体テストを構成するものとして以下の4本の柱があり、これらの掛け算でテストの価値を評価できる。
- 退行に対する保護
- リファクタリングへの耐性
- 迅速なフィードバック
- 保守のしやすさ
1.退行に対する保護
退行=機能追加や変更をした際に、既存の機能にバグがでてしまうこと。デグレ。
退行に対する保護がテストにどのくらい備わっているのかを把握するには、テスト時に実行されるプロダクション・コードの量に着目するとよい。
2.リファクタングへの耐性
リファクタリングへの耐性
= 偽陽性(テスト対象のコードは正しく機能しているにかかわらず、テストが失敗すること) が生まれづらい性質のこと
なにが偽陽性を引き起こすか?
テストコードが対象処理の最終結果だけでなく、実装の内部の詳細に目を向けていると偽陽性が発生する。

上の図の例でいくと、右側の手順に着目したテストは、最終結果は変えずに手順1→手順4→手順3のようにリファクタした場合でも失敗となってしまう(偽陽性)
偽陽性がおきるとなにがまずいか?
- リファクタ後にふるまいがかわってないことを保証するテストとなってない(テスト駆動開発などでも「レッド→グリーン→リファクタ」の手順をふむが、これが成り立たない)=安心してリファクタできない。
- もっとひどい場合、偽陽性が続くと、開発者はそのテストを無視するようになり、本当に問題があっても本番環境に持ち込まれてしまう。
退行に対する保護と、リファクタリングへの耐性の関係
退行に対する保護・・・偽陰性を防ぐ
リファクタングへの耐性・・・偽陽性を防ぐ

3.迅速なフィードバック
テストの実行時間の短さのこと。
テストの実行時間が短くなると、実行のハードルが下がり、結果的に用意されるテストケースやテスト実行の頻度が増える。
テストの実行時間が長くなると、実行のハードルが上がり、テスト実行の頻度が減って、バグが気づかれず存在する時間が長くなる。
4.保守のしやすさ
- 解析性(可読性、理解用意性)
- テスト容易性
- テストを行うためにいろんなプロセス外依存を用意しなきゃいけないとなると、テスト容易性は低い。
- コマンド1発でテストが実行できて、しかも1分以内に終わる、みたいな状況がbest
理想的なテストの探求
あらためて、テストケースが価値をもつには、以下の4本の柱を満たす必要あり。
- 退行に対する保護
- リファクタリングへの耐性
- 迅速なフィードバック
- 保守のしやすさ
テストケースの価値 = (0~1) * (0~1) * (0~1) * (0~1)
※(0~1)がそれぞれの柱に対しての充足度
ひとつでも0になる柱があると、テストケース全体の価値も0
では理想的なテスト(テストケースの価値=1)を作成することは可能か?
➡結論:No
➡️理由:4本の柱のうち、「退行に対する保護」「リファクタリングへの耐性」「迅速なフィードバック」は互いに排反する性質だから。
4本の柱のバランスをとらなきゃいけなくって、どれかが完全に0になるとアウト。1は無理でも0にならないようにする必要がある。
バランスのとり方の結論
「退行に対する保護」「リファクタリングへの耐性」「迅速なフィードバック」の3本の柱すべてを最大限に備えた単体テストを作成することは不可能である。

4本の柱のうち、
「リファクタリングへの耐性」= 0か1か、備えるか否かのどちらかしかない。
「保守のしやすさ」= 他の3本の柱と関連がない。
「リファクタリングへの耐性」は最大限担保することを狙い、「退行に対する保護」と「迅速なフィードバック」のあいだでバランスを調整するのがよい。

上記を踏まえたうえでの、ブラックボックステストとホワイトボックステストの考え方
ブラックボックステスト:ソフトウェアの内部構造を知ることなしに検証するテスト。仕様や要求をもとに作成される。
ホワイトボックステスト:ソフトウェアの内部構造に着目したテスト。ソースコードから作成される。
| 退行に対する保護 | リファクタリングへの耐性 | |
|---|---|---|
| ホワイトボックステスト | 優れている | 劣っている |
| ブラックボックステスト | 劣っている | 優れている |
リファクタリングへの耐性は0か1か。備えるか否かのどちらか。
➡️まずはブラックボックステストを選択(単体テストでも)。
優れたテストケース、プロダクションコードの設計となっていればブラックボックステストであっても、ホワイトボックステストとほぼ同等の網羅率となる。というの筆者の立場。
ちなみにホワイトボックステストで気にする網羅率(カバレッジ)にこだわりすぎるのはあまり意味がないと筆者は指摘(プロダクションコードの網羅率がいくら高くても、動作が正しいかどうかの保証にはなっていないため)
網羅率が低すぎると「退行に対する保護」が下がるので下限は設けてもいい(例えば70~80%)が、100%を目指すことは「既存機能にデグレが発生しないことを迅速に確認」という目的から考えると、効果が薄いので非推奨。
【参考】単体テストの流派:古典学派とロンドン学派
単体テストは以下の3つの性質を持つ。
- 「単体」と呼ばれる少量のコードを検証する。
- 実行時間が短い。
- 隔離された状態で実行される。
「単体」と「隔離」の解釈の違いにより、2つの学派が存在する。
| 単体の意味 | 隔離対象 | モック対象 | |
|---|---|---|---|
| 古典学派 | 1つのふるまい | テストケース | 他のテストケースの実行に影響のある共有依存(DBとかファイルシステムとか) |
| ロンドン学派 | 1つのクラス | クラス | 全ての協力者オブジェクト |
著者は古典学派のスタイルを好んでいる
➡理由
- ロンドン学派のスタイルだと単体テストがテスト対象の内部的なコードと密接に結びつく傾向があるため、リファクタリングへの耐性がなく偽陽性のでるテストとなるため。
- ロンドン学派のスタイルだと、クラスの隔離のために必要以上にモックの準備や、テストデータの用意への労力がかかるため。
考察・感想
ウォーターフォールのような変更やリファクタリングを前提としないプロジェクトの場合、いかに偽陰性がないか?だけが重視され、ホワイトボックス的な網羅率等が最重要指標として利用されていたと思われるが、変更やリファクタを前提とするアジャイル開発では偽陽性がでないテストであることが重要なんだろうな思った。
全体としてはテストコードも保守維持コストのかかる負債であるという点が一番刺さった。
質にこだわって最小限の労力で最大の価値を生み出すことを考えていきたい。
