えっ、私のテストカバレッジ、低すぎ…?

まとめ

  • テストの「カバレッジ」には、C0, C1, C2の3レベルが存在する
  • 一般的なカバレッジ測定ツールはC0しか測定できない
  • C0だろうがC2だろうが、カバレッジは目安にしかならない
    • とはいえ、他に目安になる物は何もないので、見ないよりは見た方がいい

しばらく前のShibuya.js*1が面白そうだったので指をくわえながらIRCで後輩に一席ぶった「テストカバレッジの罠」について書いておくよ。

FizzBuzzのテスト

以下のようなコードを考えよう*2

<?php
function fizzbuzz($i){
    $return = '';
    if( $i % 3 === 0 )
        $return .= 'Fizz';
    if( $i % 5 === 0 )
        $return .= 'Buzz';
    if( $i % 3 !== 0 && $i % 5 !== 0 )
        $return = $i;
    return $return;
}

なんの変哲もないFizzBuzz*3。さて問題、このコードのカバレッジを100%にするにはどうすればいいだろう?

C0カバレッジ;命令網羅

まず、$i = 1, 3, 5という入力が考えられる。このとき、コード内の全ての行が一度は実行されている。一般的なカバレッジ測定ツールでは、100%のカバレッジと判定するだろう。
全ての命令を網羅しているので、このカバレッジを「命令網羅カバレッジ」と呼ぶ。
さて、このテストは十分だろうか?まあ、十分ではない、という人が多いだろう。結果は「Fizz」「Buzz」「数字そのまま」「FizzBuzz」のどれかになるわけで、テストが3つというのは明らかに少ない(ちなみに、「FizzBuzz」になるケースが抜けている)。これを十分というのは少し無理がある。

C1カバレッジ:分岐網羅

コードを見直してみよう。このコードには分岐が3つある。という事は大雑把に言って、2^3=8通りの結果がありそうな事が予想できる。この8通りの可能性を全て網羅する、というのは割といいテスト戦略に思える。
表に書き出すとこんな感じ:

3の倍数である 5の倍数である 3の倍数でなく、かつ5の倍数でもない 代表的な値
-
× -
× -
× × 3, 6, 9
× -
× × 5, 10, 20
× × 1, 2, 4
× × × 15, 30, 45

(「代表的な値」が「-」になってるのは、その条件を満たす入力が論理的に存在しない場所)
検討の結果、8通りのうち4通りの可能性はあり得ない事が分かったので、結果としては3通りから4通りになっただけだが…ともあれ、有効な条件群は上から順に「Fizz」「Buzz」「数字そのまま」「FizzBuzz」に相当する。この基準を採用してテストを考えると、先ほどのように「FizzBuzzになるケースがカバーできていない」というケースが無くなる訳だ。
このような網羅率を「C1カバレッジ(分岐網羅カバレッジ)」と呼ぶ。テストケースが同じなら、常にC0カバレッジ=

C2カバレッジ:条件網羅

さて。さっきの条件の中に「3の倍数でなく、かつ5の倍数でもない」というAND条件が入っていた。分岐網羅カバレッジは「全分岐を一度舐めればOK」という事だったので、この条件は「○か×、成立するか成立しないか」でしか見なかったが、この条件はバラせる。つまり、この最後のif文だけを見ても

  • 3の倍数でなく、5の倍数でない
  • 3の倍数であり、5の倍数でない
  • 3の倍数でなく、5の倍数である
  • 3の倍数であり、5の倍数である

という4条件まで見ないと、なんとなくこの条件分岐をコンプリートできた気にならない。このような見方が「条件網羅カバレッジ(C2カバレッジ)」だ。最初のif文の条件をA, 二番目をB、三番目をC&&D、と見て、ABCDそれぞれの成立/不成立を網羅するので、検討する可能性は2^4 = 16通りになる。
今回の場合、たまたまAとC、BとDの条件が互いに裏返しの関係になっている、つまり「3の倍数であり(A)、かつ3の倍数ではない(C)」があり得ないので、最終的に必要となるテストケースはC1カバレッジ100%の時から増えないが。

網羅されていないケース

C2以上に網羅率の高いカバレッジは(多分)存在しない。という事はC2カバレッジを満たすコードを書いていけばバグが無いのか、というと、勿論そんな事は無い。
追記:コメント欄にて「ワシのカバレッジは七式まであるぞ」との旨コメントいただきました。コメント欄参照。

ある程度経験値のあるプログラマーなら0や負の数がテストされていない事にすぐ気づくだろうし、ぺちぱーなら「数値として解釈できる文字列」「浮動小数点」など、より大量の確認すべきケースに思い当たるだろう。数値でないものが渡ってきた時の挙動も試していない。
カバレッジ計測しながらテストをする事で測定できるのはあくまで「プログラマーが書いたつもりのコードがちゃんと書けているか」であり、それが仕様通りなのかとか、あるいはそもそも仕様に問題が無いのか、といった事は保証されない。ユニットテストの網羅性の扱いについて - 千里霧中で挙げられている「構造網羅」「仕様網羅」の問題だ。

結論

というわけで、(C0)カバレッジが完全であってもロジックにミスがある可能性は残るし、かといってC2カバレッジまで網羅したとしてもバグがなくなるわけじゃない。
それでも、

  • 機械的にコードを網羅できるカバレッジの考え方は、堅牢なプログラムを作る上では大事だし、
  • C0は機械的に測定しやすい=自動テストに組み込んで確認しつつコードを書ける、というメリットがある。
    • たとえC0が「FizzBuzz」すら逃すようなザル基準であったとしても。

というわけで、テストカバレッジは目安にしかならないので依存してはいけないが、目安程度にはなるし、他に目安になる物もないので、気にはしましょう、というお話。

For More Information

C1やC2カバレッジのような水も漏らさぬテストは確実性が高いけど、代わりに実行時間が指数関数的に跳ね上がっていく。というわけで、完全性を諦めた上で、経験則に基づいて網羅すべき条件を間引いていく技術、というのが存在する。「ペアワイズ法」とか呼ばれるものなのでこの辺でぐぐれ。
また、今回の記事はコードの中身を知っている前提のテスト(ホワイトボックステスト)だったけど、コードの中身を知らない人がどう網羅的に、かつ現実的なテスト量で不具合を発見するか、という方法論もある。こちらは書籍が山のようにあるのでパス。

*1: さいきんの JavaScript テスト / Test.js - Shibuya.js 発表資料 - 2nd life

*2:PHPなのはブクマ数が伸びやすいからだ

*3:中カッコを取っ払うのは個人的には好きじゃないのだが、サンプルコードなので見やすさを重視した