はじめに
ZOZOMO部プロダクト開発ブロックの木目沢です。
ZOZOMOで提供しているZOZOTOWN上での「ブランド実店舗の在庫確認・在庫取り置き」APIの開発に携わっています。
今回は、開発当初から現在に至るまでのユニットテスト戦略についてお話しします。
意識してテストを書いていたのにカバレッジが低い問題
2021年11月にリリースされたブランド実店舗の在庫確認・在庫取り置きの機能ですが、開発当初のユニットテスト方針は以下のようなものでした。
- モデルのユニットテストは必ず書く
- モデル以外の箇所は可能な範囲でユニットテストを書く
当時は実装のコードよりテストコードを先に書くといった文化はなく、レビューでテストの有無や内容を指摘する程度のものでした。
カバレッジも取っており、GitHub上では見える化していたものの、いつの間にか確認する機会も失われていきました。
もちろん、リリース前にはQAチームによるUIのテストも通り、十分なテストを経てリリースされています。しかし、当時のカバレッジは60%程度。カバレッジの数値というのは結果であってカバレッジの数値を上げることが目的ではないものの、今後安全に保守していくには心もとないものでした。
カバレッジは何%あるのが妥当か?
60%で心もとないと思ったのは、以前マーチン・ファウラー氏のテストカバレッジに関するブログを読んだことがあったためです。ポイントを引用します。
思慮深くテストを実施すれば、テストカバレッジはおそらく80%台後半か90%台になるだろう。 カバレッジの数値が低い場合、たとえば50%以下の場合は、おそらく問題があるだろう。高いカバレッジの数値にはあまり意味はない。ダッシュボードの数字に意味がなくなる助けをするだけだ。 以下の質問に「はい」と答えられるならば、おそらくテストは十分だろう: 本番環境で発見されるバグはほとんどない。そして、 本番環境でバグを出すことを恐れてコードの変更をためらうことがない。
50%以下ということはありませんでしたが、それでも「本番環境でバグを出すことを恐れてコードの変更をためらうことがない」とは言えない状況でした。
TDDとTDD is deadへの誤解
カバレッジを上げる必要があると考え、まず思いついたのは「テスト駆動開発(以下TDD)」でした。テストを先に書けば自ずとカバレッジが上がると考えました(後述しますが、この考えは間違っています)。
一方で、同時に「TDD」に関して思い出したのは、「TDD is dead. Long live testing.」という言葉でした。2014年に発表されたRuby on Railsの作者としても有名なDavid Heinemeier Hansson氏のブログです。
インパクトのあるタイトルが界隈を賑わせましたが、タイトルだけで判断すると誤ります。そして実際誤っていました。TDDは意味がない。最終的にテストがあればよいのだと。
ブログの記事をよく読むと、以下のことが述べられています。
伝統的な意味でのユニットテストはほとんどしない。テストファースト原理主義
ユニットテストやテストファーストという表現に「伝統的な意味での」という接頭辞や「原理主義」という接尾辞がありました。伝統的とか原理主義というのは、どのような意味なのか、以下抜粋します。
私は伝統的な意味でのユニットテストはほとんどしない。すべての依存関係をモックにし、何千というテストが数秒で終わるようなユニットテストのことだが。テストファーストのユニットテストは、中間的オブジェクトや間接的で過剰に複雑な構造を生みがちだ。「遅い」ものをすべて避けようとするのがその理由で、データベースやファイルIOなどを避ける。ブラウザを使ってシステム全体をテストするのも避けようとする。
批判しているのは、モックを大量に使ってすべてをテストファーストで設計する手法であったり、データベースやE2Eテストも避けようとするやり方であったりします。
書籍「テスト駆動開発」の付録では、TDDの歴史の流れから説明されていて、David氏がどのような経緯でこの考えにたどり着いたかが詳しく説明されていますのでご一読されることをおすすめします。
もう一度TDD。そしてTDDとは何か?
書籍「テスト駆動開発」にきちんとTDDとはなにかということが記載されています。
開発者が設計の治具としてテストコードを同時に書きながら開発と改善を回していくというTDDの姿(KentBeck/テスト駆動開発より)
これをもう一度見直そうと書籍を読み直しました。
そこで、気がついたのは以下のようなTDDにおけるテストと実装の流れでした。
- テストを書く。実行すると実装前なのでエラーになる(RED)
- グリーンとなる実装を書く(GREEN)
- リファクタリングし、詳細に実装していったり、整えたりする(REFACTORING)
- 以下繰り返し
つまりテストを最初に書くテストファーストだけがTDDのすべてではなく、実装し、テストを頼りにリファクタリングとしていく全体の流れこそがTDDです。
プロジェクトへのTDD導入
ここまで理解したところで、TDDをプロジェクトに導入しました。徐々にカバレッジも上がりましたが、あくまでTDDはタスクを実現するための設計手法であって、品質を保証するためのものではありません。テストを先に書けば自ずとカバレッジが上がると当初は考えましたが、TDDとカバレッジの関連は本来ありません。もちろん、TDDによって他のテストも書きやすくなり、結果的にカバレッジが上がっていくことはあると思います。
TDDを主にモデル層に導入したところ気がついたのはビジネスロジックが「モデルだけ」に集まることです。タスクを実現する実装コードをモデルだけで実現するように書いていくことで、他の層やSQLに書かれることがなくなりました。 この点がTDDにおいて重要なところになります。テストによって設計を駆動していく手法、まさに、Test Driven Developmentということです。
データベースのテストもAPIのテストもやる
TDDを進めていくにつれて、データベース接続のテストや、APIのコントローラーのテストなどモデル以外の層のテストも書くようになっていきました。この辺りをモックにしてテストの実行時間を短くするという考えもあるかと思いますが、それこそまさにDavid氏が批判してきたところです。
現在では、データベースのテストもJavaであればDBUnitを使うことで、Spring BootやMyBatisと連携したテストが書けます。コントローラもSpring BootではWebMvcTestが容易に使えるなど、テストツールが充実しています。また、時間がかかるテストもCIで回せばそこまで大きな負担とはなりません。
ユニットテストに加えて、E2EのテストもKarateなどで容易に導入できます。弊チームでも導入し始めており、仕様の確認から実装後の確認までE2Eテストを活用しています。
こうして、プロジェクトではTDDやその他ツールを活用し、充分に成果があがるようになってきました。ここでさらに社内にもTDDを広めようと2つの活動を開始しました。
続きはこちら