読者です 読者をやめる 読者になる 読者になる

高いコードカバレッジを無理なく確保するために

 前のエントリでコードカバレッジの基準について議論があったので補足したいと思う。
 なお今回はほんとに基本的な内容で、当たり前だと感じる人も多いかもしれない。ただ現実問題、問題が繰り返されている例もかなりあるようなので文章として書き出すことにした。あと内容はコードカバレッジ100%の不毛性やコードカバレッジの価値の議論と一部リンクしているけれど、長くなってしまったので解説は別の機会にまわしたい。

コードカバレッジを無理なく上げるアプローチ

 とりあえずユニットテストで高いコードカバレッジを無理なく確保するためにはいくつか留意すべきことがある。

 まず常識的に、製品要求としてユニットテストのテスタビリティを定義して、トップダウンでテスタビリティを作りこむ必要がある。テスタビリティを考慮せずに作られたコード相手に、後からコードカバレッジを確保しようとするのはかなり困難だからだ。

 一方、本エントリの主題だけれど、テストから気づきを得てテスト対象を改善していくフィードバックループも重要だ。例えば単純な例として、以下のようなC++のコードを扱う場合を考える。

...
if (Motor::LockError()) {
//Motorは外部提供のブラックボックスコンポーネント
//ユニットテストの環境ではMotor::LockError()は常にFALSE
...(LockError時のコード)...
}
...

 「テストから気づきを得てテスト対象を改善する」とは、ユニットテストを作成・実行する中で「Motor::LockError()」のような問題を見つけたら、「MotorをDIで渡す」「Motorをラッパーで置換し、ラッパー内で切替可能にする」といったリファクタリングでそれを解消してしまう、というアプローチだ。
 テストを書くことで明らかになるテスタビリティの問題は少なくない。というのも最初から完璧かつ網羅的にテスタビリティを作るのは困難であるし。またここでも少し触れたがユニットテストの作成はプログラミング行為そのもので、本質的にテスト対象とのインターフェースにリファクタリングの余地を抱えているためだ。そのため、上記の「テストから気づきを得てテスト対象を改善する」アプローチを積極的に実践できる環境は、コードカバレッジの確保においてとても重要だ。

無理の出る環境

 逆にそういったテスト対象の変更が許されず、テストコード側の工夫だけでコードカバレッジを上げていくしかない環境では色々無理が出てくることになる。

 例えば以下のようなプロセスを取っていると、そういった状況になるだろう。

  1. 実装工程:すべてのテスト対象を設計・実装
  2. (テスト対象をFix)
  3. テスト工程:すべてのユニットテストを設計・実装

 こうしたウォーターフォール的な工程分けは、テスタビリティに関して以下の問題を抱える。

  • テストで得られる気づきが後工程に集中する
    • 前述の通り、ユニットテストではテスト実装・実行中にテスタビリティ上の問題に気づく場合が多々ある。この工程分けではそのタイミングがテスト対象Fix後になってしまう。
  • テスト工程からの手戻りコストが増加する
    • 例えば上記のテスト工程で問題が見つかると、実装工程に差し戻して修正し、またテスト対象をFixしてテスト工程をやり直すことになる。

 この「問題は出るけど、手戻りは嫌だ」という状況を作りだしてしまうと、しばしばテスト対象を変えずにテストコードの作りこみで問題に無理やり対処することになる。作りこみ方法としては、深刻な順に並べると例えば以下のようになるだろう。

  1. 既存の環境で何とかする
    • 例えばプリプロセッサ、リフレクションといった強権的な言語機能を使って、なんとかテストする。
  2. 特別な環境で何とかする
    • 例えば特定のテスト対象のために別途テストプロジェクトを新設して、プリプロセッサマクロを追加したり、classpathを変えたりする。
  3. テスト対象に手を入れて何とかする
    • 例えば、テスト対象をコピペし、その中のコードを削除したり置換したりして、妥当なコードカバレッジが得られるものに変えてしまう。ただし結果ではテスト対象を変えていないものとして扱う。
  4. テスト結果に手を入れて何とかする
    • コードカバレッジの測定値を水増ししたり、テスト仕様を偽装したりする。

 こうした無理はいずれも冗長なコストやリスクを発生させがちだ。例えば4ではプロセスが完全に形骸化してしまっていて、ユニットテストの信頼性そのものが怪しくなる。3も主観や変更ミスのリスクが高い上、テストの保守を大いに煩雑化させてしまう。一方、1、2についても、頼りすぎるとテストとテスト対象が広く・深く結合してしまい、テストの保守・運用工数が増大するリスクを持つ。

 これらの積み重ねでリスクや過剰な工数を増やしてしまうと、コードカバレッジ確保のコストがその利益を大きく逆転してしまい、コード網羅の必要性そのものが微妙になってくる。またそこでは明らかに、テスト対象をリファクタリングして済ませる方法より、より多くの手間がかかることになるだろう。

無理のでない環境

 こうした無理を避け「テストから気づきを得てテスト対象を改善する」状態を作る方法としては、ユニットテストの設計の前倒し、実行の前倒しと、テスト工程の反復が有効だ。
 例えば以下のような考え方や方法論の実践は、無理のないコードカバレッジ確保にとても有効だ。

  1. テスト駆動開発。まずテストを書いて、それに製品コードを合わせるという方法で、最初からテスタビリティに優れたコードを生み出す。
  2. 自分の書いたコードにはユニットテストを書くという文化。例えばコミット時はコードとそれに対するテストのペアのコミットを要求する。
  3. インクリメンタルなユニットテストの作りこみ。変更が落ち着いたコード等から、巡回的にユニットテストのレビュー&作りこみを行う。
  4. イテレーション開発。イテレーションごとに単体テスト工程を実施し、問題が出たら次イテレーションで改善する。

 なお1、2のような開発者個人レベルの方法に関しては、テスト全体の整合性や網羅性を確保できると保証できない。が、それでも今回扱っている課題の大部分を解消してしまうだろうし、3、4などといった上位のプラクティスを強力に支援する。やらないよりははるかに良い。


 まとめとして、本当に基本的なことだけれど、コードカバレッジの効率的な確保には「テストで得られる気づき」と「それに基づいたリファクタリング」が不可欠だ。逆にそれができない状態では、高いコードカバレッジの確保に色々と冗長なリスクやコストが伴うことになるだろう。