Skip to content

Goのテストカバレッジ

Go blogにはカバレッジに関連する2つの記事がある。

それぞれの概要をみていく。

Goが誕生した初期からツールによるサポートを念頭に置いて設計した。

  • パースしやすい言語構文、パッケージシステム、標準ライブラリに言語パーサを持つなど
  • ツールの例としては gofmt, godoc, go vet などがある

Go 1.2 (2013)のリリースではテストカバレッジツールが導入された。カバレッジ計測は、テスト対象となるバイナリが持つ分岐にブレークポイントをセットしておき、分岐が実行されたら取り除く手法がある。テストを最後まで実行して、それでも残っているbreakpointが実行されていない分岐となる。この手法は一般的であり、GNU gcovなども採用しているが、課題もある。

  • バイナリを解析するので難しさがある
  • 分岐とソースコードの対応も難しい
  • アーキテクチャごとに対応方法が変わるので流用できない

そのため、Goの go test -cover では、ビルド前にソースコードを書き換えて分岐の通過をカウントしておき、最後に結果を出力する方法を採用した。GoCover カウンタがコンパイラによって埋め込まれた行を表す。

package size
func Size(a int) string {
//GoCover.Count[0] = 1
switch {
case a < 0:
//GoCover.Count[2] = 1
return "negative"
case a == 0:
//GoCover.Count[3] = 1
return "zero"
case a < 10:
//GoCover.Count[4] = 1
return "small"
case a < 100:
//GoCover.Count[5] = 1
return "big"
case a < 1000:
//GoCover.Count[6] = 1
return "huge"
}
//GoCover.Count[1] = 1
return "enormous"
}

このテストを実行するとカバー率を表示する。

Terminal window
% go test -cover
PASS
coverage: 42.9% of statements
ok size 0.026s

パッケージ全体のカバレッジ以外では、HTMLで表示したり関数単位のカバー率も取れる。

Terminal window
# カバーした範囲を出力しておく
% go test -coverprofile=coverage.out
# HTMLで表示
% go tool cover -html=coverage.out
# 関数ごとに表示
% go tool cover -func=coverage.out
size.go: Size 42.9%
total: (statements) 42.9%

-covermode= オプションで「カバーしたかどうか」の他にも「何度通過したか」も計測できる。

  • set: 一度でも通過したかを記録する
  • count: 通過した数をカウントする
  • atomic: count と似ているが並行プログラムでも正しくカウントする

Go 1.2でカバレッジツールが導入されたが、その時点では、バイナリをビルドしてシステム全体をテストする「結合テスト」のようなことは実現できなかった。Go 1.20以降は go build -cover オプションが追加されたのでバイナリを使ったテストも可能になった。

golang-commonmark/mdtoolを使った例をみていく。

testdata 以下の *.md ファイル全部を与えて、プログラムがクラッシュしないかをみるテストを書く。しかし以下のスクリプトでは、クラッシュしないことは分かるが、どれくらい網羅したか分からない。

#!/bin/sh
# usage: integration_test.sh [coverargs]
# 完全な例は元記事をみること
BUILDARGS="$*"
go build $BUILDARGS -o mdtool.exe .
FILES=$(find testdata -name "*.md" -print)
N=$(echo $FILES | wc -w)
for F in $FILES
do
./mdtool.exe +x +a $F > /dev/null
done
echo "finished processing $N files, no crashes"

integration_test.sh は引数でビルドオプションを渡せるようになっている。カバレッジ取得のため、go build にカバレッジ計測のためのオプションを渡すラッパースクリプトを作る。

wrap_test_for_coverage.sh
#!/bin/sh
set -e
PKGARGS="$*"
rm -rf covdatafiles
mkdir covdatafiles
GOCOVERDIR=covdatafiles /bin/sh integration_test.sh -cover $PKGARGS
go tool covdata percent -i=covdatafiles

実行結果は以下のようになる。

Terminal window
$ wrap_test_for_coverage.sh
gitlab.com/golang-commonmark/mdtool coverage: 48.1% of statements

ここで重要なところは、

  • go build-cover オプションが渡っている
  • GOCOVERDIR= 環境変数にディレクトリ名がセットされている
    • これをセットしていない場合はバイナリを実行してもカバレッジを取らない
  • go tool covdata percent-i オプションで GOCOVERDIR= の場所を与えている
  • 結果は自分で消さない限り GOCOVERDIR= に残る
    • 必要なだけコマンドを実行してから go tool covdata する

その他にも、特定のパッケージだけを計測する -coverpkg オプションなどもある。

Terminal window
go build -cover -coverpkg=gitlab.com/golang-commonmark/markdown,gitlab.com/golang-commonmark/mdtool

go build -cover のファイルフォーマットは go test -coverprofile= のテキストフォーマットとは異なるため go tool cover コマンドに渡すことができない1。そのため、go tool covdata を使って、go tool cover が期待するテキスト形式に変換してから使う必要がある。

以下に変換するコマンド例を示す。

Terminal window
go tool covdata textfmt -i=covdatafiles -o=cov.txt
go tool cover -func=cov.txt

Merging raw profiles with ‘go tool covdata merge’

Section titled “Merging raw profiles with ‘go tool covdata merge’”

go build -cover でビルドしたバイナリは、実行するたびに GOCOVERDIR= 以下へ1つ以上のファイルを出力する。異なるテスト対象を実行した場合などは重複してカウントされるなどが起きるので、go tool covdata merge でテスト結果をマージすると便利だろう。

Terminal window
$ ls covdatafiles
covcounters.13326b42c2a107249da22f6e0d35b638.772307.1677775306041466651
covcounters.13326b42c2a107249da22f6e0d35b638.772314.1677775306053066987
...
covcounters.13326b42c2a107249da22f6e0d35b638.774973.1677775310032569308
covmeta.13326b42c2a107249da22f6e0d35b638
$ ls covdatafiles | wc
381 381 27401
$ rm -rf merged
$ mkdir merged
$ go tool covdata merge -i=covdatafiles -o=merged
$ ls merged
covcounters.13326b42c2a107249da22f6e0d35b638.0.1677775331350024014
covmeta.13326b42c2a107249da22f6e0d35b638
  1. カバレッジデータのフォーマットや操作はCoverage profiling support for integration testsにまとまっている