以下のリーンテスト(Lean Testing)に関する記事の要約です。
Lean Testing or Why Unit Tests are Worse than You Think
具体的なテスト手法の紹介がないため概念的な話になってますが、以下のような人にお勧めです。
- ユニットテストがモック地獄、スタブ地獄になっている
- C0/C1 コードカバレッジ100%に疑問を感じる
- 開発中はユニットテストばかり書いてる気がする
- E2Eテストや結合テストの方が簡単、または効率が良いように感じる
ユニットテストについてはやや手厳しい内容になっているため、ユニットテスト大好きな方は注意してください。
原文には引用も各種参考文献&動画も多いため、この要約を読んで興味をもったら原文を読むことをお勧めします。
要約の要約
結論:リーンテストでは、ROIの高いE2Eテストと結合テストを優先する。ユニットテストは弊害も多いため、過信せず本当に必要なもの(ex. エッジケースが存在する複雑な純粋アルゴリズム的なコード)だけにする。 "Be economic. Be lean."
テストの経済的コストを考えた場合、最もROIが高いテストを優先することが望ましい。ROIが高いテストとは「コスト(Cost)」や「スピード(Speed)」よりも**「信頼性(Confidence)」が高いものであることが重要**となる。
E2Eテストの信頼性が最も高く、結合テストはコストとスピードと信頼性のバランスが最も優れているためROIが高くなる。ユニットテストは個々の信頼性がとても低いため、コストやスピードには優れていてもROIが低くなる。
要約
テストの信頼性
- テストは以下の3つの観点で評価する。
- コスト (Cost)
- スピード (Speed)
- 信頼性 (Confidence)
- テストが「実際のソフトウェアの使い方」に近いほど、テストの信頼性が高くなる。
- E2Eテストが信頼性が最も高いが、コストやスピード面は不利。
- 結合テストが信頼性、コスト、スピードのバランスが最もよい。
- ユニットテストはコストとスピード面では優れているが、信頼性は最も低い。
テストのROI
- ROI(Return on Investment)は、E2Eテストが最も高く、結合テストがその次に高い。どちらもユニットテストに比べるとROIは遥かに高い。
- E2Eテストと結合テストのROIが高いのは、より広範囲のコードベースを網羅できるため。テストのコストを考慮しても、前述したテストの信頼性は極めて高いものである。
- E2Eテストはユーザーが実際に使用するクリティカルパスをテストするため、より信頼性が高くなる。
- ユニットテストは個々の部分の動作は保証するが、全体の動作は保証しないため信頼性が低くなる。
「結合テスト」と「E2Eテスト」という用語
- 結合テスト(Integration Test)とE2Eテスト(End-to-End Test)という用語は一部の人間に大きな恐怖を与えてしまう。テストが脆く、セットアップが困難、テストに時間がかかると考えられてるためである。
- 上述した課題を回避するためには、モック要素を極力排除することが大事。
- Reactの結合テストでは複数のコンポーネントを対象とする。本物をより多く使うことで、モックを減らし、実装の詳細をテストすることを回避できる。
- バックエンド開発であれば、本物のDBサーバとHTTPリクエストを使用する。例えばDockerでDBコンテナを立ち上げることで、テスト毎に実施することが容易になる。これによりコードを変更した場合でも、より速く、より簡単に、より信頼できるテストを行うことができる。
コードカバレッジ(ex. C0/C1網羅)
- **コードカバレッジにより得られるメリットは漸減的(徐々に減っていく)**である。
- 仮に開発対象のコードカバレッジが100%であっても、依存ライブラリのコードカバレッジがそうである保証はない。そして理論上は依存ライブラリのコードカバレッジは0%ということもあり得る。
- 特殊ケースのバグにより0.1%のユーザーに影響与えてもビジネス的には生き残ることはできるかもしれないが、コードカバレッジの負担により納期に間に合わなければビジネス的には生き残るのは難しいかもしれない。
コード品質とユニットテスト
- ユニットテストはアンチアーキテクチャ要素である。
- コードの内部構造を硬直化するため、ユニットテストは保守面の負債を増加させる。
- 実装したモジュールとユニットテストの間で高い結合(Coupling)が発生する。ユニットテスト自体がモジュールみたいなものになる。
- ユニットテストの実施しやすさを重視して、より適切な設計を選択しないようになる危険性がある。ユニットテストを容易にするためだけに「不要な間接的なレイヤ」を導入するのも一例。
記事対する雑感
ある開発でモック地獄と化したユニットテストの半分を結合テストに置き換えた経験があり、そのメリットの大きさも理解しているため、原文の記事の内容については概ね賛同します。
- 結合テストが最もバランスが良くROIが高いのは特に同意するところ。
- 外部依存がなかったり状態をもたなかったりするモジュールについてはユニットテストは有効かつROIが高い。こういうモジュールが大半を占める開発ではユニットテスト中心で全く問題ない。
- モック地獄と化したユニットテストはほとんど無意味。ROIが低く保守負債が積みあがるテストになってしまう。「テストの作り直し工数がかかりすぎるので、この機能追加/変更はできません」という本末転倒な状況に陥ることも実際にあった。
- ユニットテストのしやすい実装、モック化などは意外とハードルが高い。技術不足または経験不足の開発者だと特に厳しいという印象。
- JUnitで結合テストを動かす枠組みを作り上げることで、結合テストでもコードカバレッジを自動的に取ることができる。そしてエッジケースのみモックを使用することで、特殊ケースも網羅できるテストを実現できる。(TODO:この辺りは簡単な例をもとに記事にしたい)
- ユニットテストのような小回りと柔軟性、結合テストのような信頼性の高さの良いとこ取り。
- JUnit上で結合テストとして使えるためのモジュール設計はできる必要がある。ただしユニットテストのしやすい実装、モック化などができるレベルであれば、そのような設計は十分にできる。
- モックは極力作らないか、テスト対象のできるだけ外側をモック化すると実装変更時のテスト影響が少なくなる。またモック外側にすればするほど、その内側の結合テストではより多くの本物のモジュールがテストされるようになる。
- Javaのデータベース処理のテストなら、テスト対象クラスのより外側(ex. JDBCインタフェース、DBサーバ)等をモック化する。
- ユニットテストは基本的にテスト対象そのものの処理をモック化するため、上述したような「外側をモック化する」という手法を取るのが難しい。だからこそ結合テストという選択になる。
- 開発内容に依存するが、結合テストで検出できずユニットテストで検出できるバグは少ない(ただしエッジケースは検出できるバグが多くなる)。特に正常ルートでは結合テストで事足りることが多い。