過去に何度か書いているが、普段からgccとgcovをよく使っている。基本的にCプログラマなので仕事でCで書くことが多く、書いたソースのコンパイルチェックや単体テストにMinGWを使っているのだ。gcovを使うと未実行のパスが可視化されて便利なので、使っている人は結構いると思う。
gcovを実行すると、詳細な情報は*.gcovなログファイルに出力されるのだが、それとは別に標準出力に要約された情報が表示される。例えばこんな感じ。
$ gcov bsearch_sample.c File `bsearch_sample.c' Lines executed:96.15% of 26 bsearch_sample.c:creating `bsearch_sample.c.gcov'
bsearch_sample.cというソースファイル全体のカバレッジが表示されている。
gcovにオプション-fを付けると、各関数の要約された情報も出力される。
$ gcov -f bsearch_sample.c Function `compare' Lines executed:88.89% of 9 Function `main' Lines executed:100.00% of 17 File `bsearch_sample.c' Lines executed:96.15% of 26 bsearch_sample.c:creating `bsearch_sample.c.gcov'
これはこれで便利なので、大抵-fを付けている。
ところで、一応組み込み絡みの仕事をしているからか、私の周囲では単体テストというと「関数ごとに最低でも命令網羅」だ。とはいえ大抵は分岐網羅でテストするし、グレーボックス的に境界値分析でテストパターンを考えて実施することも多い。世の中にはもっと厳しいテストを実施している所も多いと思うが、私がいる所は組み込み関連とはいえ要求される品質基準がそれほど高くない所なのだ。
さて、個々の関数レベルでのテストが要求される場合、世間一般ではCのstatic関数の単体テストはどうやって実施しているのだろう? 思いつく方法はこんなところ。
- 公開インタフェース部の関数経由で、頑張って何とかテストする。
- デバッガ上で手動でチェックする。
- 該当するファイル内に単体テスト用のルーチンを書き、ビルドして実行する。
- 別途単体テスト用のプログラムを用意し、そのソース中でテスト対象のソースファイルをインクルードする。
私の場合(4)の方法を採用している。この場合、gcovで要約情報を表示させると、テスト対象のファイルや関数だけでなく単体テスト用プログラムのソースファイルや関数の情報も表示されてしまう。なのでテスト対象に関する要約情報のみ抜き出すようにフィルタリングしている。
gcovの要約情報は空行で区切られているので、空行をレコードの区切りとしてテキストストリームを読み込むようにすれば、各情報を1件ずつ読み込むことができる。awkやPerlなどなら簡単な話だ。
問題はどうフィルタリングするか? この辺りは関数の命名規則等のローカルなルールの話になってくる。
例えば私の周囲では、Cでモジュールを作る場合、名前の衝突を避ける為に関数・定数・typedefで付ける別名にはプレフィックスとしてモジュール名を付けることが多いので、それを逆手に取ってモジュール名でフィルタをかけている。
例えば、こんな場合。
- テスト対象のソースファイルはfoo.cという名前。
- 単体テスト用のコードはfoo_unittest.cという名前。実行ファイル名はfoo_unittest。
- foo.c内の全関数には、"foo_"というプレフィックスが付加されている。
# gawkを使う場合 gcov -f foo_unittest | gawk 'BEGIN {RS = ""; ORS = "\n\n"} /`foo_/ || /foo\.c/' > coverage.txt # Rubyの場合 gcov -f foo_unittest | ruby -00 -n -e 'print if /`foo_/ || /foo\.c/' > coverage.txt
但し、テスト対象モジュールの全関数にプレフィックスが付いているとは限らない。その場合は、単体テスト用プログラム側の全関数にプレフィックスをつける。フィルタする側では、そのプレフィックスで始まる関数の情報を除外する。
先ほどの例で、単体テスト用プログラム側の全関数に"test_"というプレフィックスがついていると仮定すると、こんな感じになる。
# gawkを使う場合 gcov -f foo_unittest | gawk 'BEGIN {RS = ""; ORS = "\n\n"} /foo\.c/ || !/`test_/' > coverage.txt # rubyの場合 gcov -f foo_unittest | ruby -00 -n -e 'print if /foo\.c/ || !/`test_/' > coverage.txt