この記事はjUnit等のユニットテスト・フレームワークの使い方は分かるが、効果的なユニットテストを書くにはどうすれば良いかが分からない、という方向けとなります。
なぜユニットテストを書くのか?
現在、多くの現場でユニットテストを書くのは当然のもの、という認識になっているかと思います。
では皆さんは、なぜユニットテストを書くのかと問われたら、何と答えるでしょうか?
多くの方は以下の様に答えるのではないかと思います。
- 書いたコードのバグを早期に発見するため
- 書いたコード、納品するコードにバグが無いことを確認するため
もちろんこの目的は重要ですが、バグ発見率のためにコードカバレッジを重視すると、
レアケースのコードを通すための作業時間が膨らむ等、テストが開発の足かせになることがあります。
ユニットテストは、書き方を知っているだけではなく、
目的を理解し正しく運用することで良い効果を発揮します。
では、何を目的にユニットテストを書くべきなのでしょうか?
もう少し掘り下げてみようと思います。
プロダクションコードの設計評価
テスト駆動開発によって注目されるようになった考え方かと思います。
ユニットテストを組みづらいということは、コードが密結合しているためであり、
変更や機能追加が困難な、低品質なコードである、という考え方です。
しかし、高品質なコード=ユニットテストがしやすい、というのは正しいのですが、
ユニットテストがしやすい=高品質なコードとは限らない、というのが難点です。
ユニットテストがしやすいコードは外部から動作を確認できるというだけであり、
機能の凝集度や結合度とは無関係です。
ソフトウェアの開発を持続可能なものにする
ソフトウェアを常に成長可能な状態に保ち、開発を持続可能にする。
これがユニットテストを書くことで成し遂げたいことです。
ソフトウェアは時とともに成長し、複雑度を増していきます。
この複雑度を抑えるためにコードの整理やリファクタリングなどの処置が常に必要になります。
もしユニットテストが無ければ、コードの整理、リファクタリングによって、
バグが混入しても気づくことが困難になります。
機能追加によって既存の機能にバグが混入しても気づけません。
場合によっては、バグの修正により新たなバグを混入させる、という悪循環が発生することもあります。
品質の良いユニットテストがあることで、バグが混入していないことを確認しながらコードの修正を実施できるようになります。
品質の良いユニットテストとは何か?
過去に携わった開発プロジェクトで、ユニットテストの20%程度が常に不合格になっており、
それらが放置されていたことがありました。
ユニットテストの件数が数千有り、全てを修正するのは現実的に不可能でした。
チームメンバーはその状態を受け入れており、機能追加時は不合格になるケースが増えなければ良い、
というルールで運用されていました。
これではユニットテストによるテストは崩壊しています。
この状態では、元々テストが不合格になっている機能に対して、
新たなバグが混入しても気づけない可能性が高いです。
致命的なバグを見逃してしまう可能性もあります。
なぜこのような状態になってしまったのでしょうか?
明らかに何かが間違っていたはずです。
テストと開発サイクル
そのプロジェクトでも、最初は全てのテストケースが合格していたはずです。
ある時から不合格のケースが出始め、
テストケースの修正よりもプロダクションコードの修正を優先した結果、取り返しのつかない状態になってしまったはずです。
(私が所属した時点ですでにユニットテストは崩壊していましたので、詳しい経緯は分かりません)
ただ、正しく開発サイクルにユニットテストが組み込まれていれば、このような事態は発生しなかったはずです。
最小限の保守コストで最大限の価値を生み出す
ユニットテストは網羅率等ではなく、最小限の保守コストで重要なロジックをテストできているかで評価するべきです。
網羅率を上げるためにgetter/setterのユニットテストを頑張って書くことに意味はありません。
それよりももっと意味のあるビジネスロジックのテストに注力するべきです。
ユニットテスト自身のリファクタリング
もしプロダクションコードの進化に対してユニットテストの修正が足かせになっているのであれば、
ユニットテストのリファクタリングを検討するべきです。
場合によっては、重要度の低いテストケースを削減するべきかもしれません。
少なくとも常に不合格になり、修正する余力も無いテストケースは削除するべきです。
それによって、保守コストと生み出す価値のバランスを取り直し、
ユニットテスト自身も改善していく必要があります。
ユニットテストの評価軸
ユニットテスト自身の評価軸はいくつかありますが、
その一つとして、プロダクションコードのリファクタリングへの耐性があります。
これは、プロダクションコードのリファクタリングに対して、バグを正しく検知でき、
かつバグではない動作をバグだと誤検知しないという特性です。
この特性が無いと、プロダクションコードの修正に対してユニットテストのメンテナンスコストが跳ね上がります。
もしかすると、問題のプロジェクトもこの特性が不足していたために、
ユニットテストのメンテナンスコストに耐えられず、崩壊してしまったのかもしれません
ユニットテストの評価軸については、別の記事でより詳しく掘り下げようと思います。
ユニットテストは契約をテストする
ユニットテストでテストするべきなのは、コードの各行の処理内容ではありません。
クラス/メソッドの契約内容です。
コードの各行をテストすると、リファクタリングに対してもろく、些末な処理に対するテストにコストをかけることになります。
そうではなく契約、つまりクラス/メソッドが満たすべき機能のテストに注力することで、
リファクタリングへの耐性があり、より意味のあるユニットテストを書くことができます。
まとめ
以下、本記事のまとめです。
- ユニットテストは開発を持続可能にするためのもの
- ユニットテストは最小限の保守コストで最大限の価値を生み出すことを目標とする
- ユニットテストはクラス/メソッドの契約をテストする