Goのテストカバレッジ
Go blogにはカバレッジに関連する2つの記事がある。
- The cover story
- [Code coverage for Go integration tests](https://go.dev/blog/integration-test-coverage
それぞれの概要をみていく。
The cover story
Section titled “The cover story”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"}このテストを実行するとカバー率を表示する。
% go test -coverPASScoverage: 42.9% of statementsok size 0.026sパッケージ全体のカバレッジ以外では、HTMLで表示したり関数単位のカバー率も取れる。
# カバーした範囲を出力しておく% go test -coverprofile=coverage.out
# HTMLで表示% go tool cover -html=coverage.out
# 関数ごとに表示% go tool cover -func=coverage.outsize.go: Size 42.9%total: (statements) 42.9%-covermode= オプションで「カバーしたかどうか」の他にも「何度通過したか」も計測できる。
- set: 一度でも通過したかを記録する
- count: 通過した数をカウントする
- atomic: count と似ているが並行プログラムでも正しくカウントする
Code coverage for Go integration tests
Section titled “Code coverage for Go integration tests”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 $FILESdo ./mdtool.exe +x +a $F > /dev/nulldoneecho "finished processing $N files, no crashes"integration_test.sh は引数でビルドオプションを渡せるようになっている。カバレッジ取得のため、go build にカバレッジ計測のためのオプションを渡すラッパースクリプトを作る。
#!/bin/shset -ePKGARGS="$*"
rm -rf covdatafilesmkdir covdatafilesGOCOVERDIR=covdatafiles /bin/sh integration_test.sh -cover $PKGARGS
go tool covdata percent -i=covdatafiles実行結果は以下のようになる。
$ wrap_test_for_coverage.shgitlab.com/golang-commonmark/mdtool coverage: 48.1% of statementsここで重要なところは、
- go build に -cover オプションが渡っている
- GOCOVERDIR= 環境変数にディレクトリ名がセットされている
- これをセットしていない場合はバイナリを実行してもカバレッジを取らない
- go tool covdata percent に -i オプションで GOCOVERDIR= の場所を与えている
- 結果は自分で消さない限り GOCOVERDIR= に残る
- 必要なだけコマンドを実行してから go tool covdata する
その他にも、特定のパッケージだけを計測する -coverpkg オプションなどもある。
go build -cover -coverpkg=gitlab.com/golang-commonmark/markdown,gitlab.com/golang-commonmark/mdtoolcovdataの相互運用
Section titled “covdataの相互運用”go build -cover のファイルフォーマットは go test -coverprofile= のテキストフォーマットとは異なるため go tool cover コマンドに渡すことができない1。そのため、go tool covdata を使って、go tool cover が期待するテキスト形式に変換してから使う必要がある。
以下に変換するコマンド例を示す。
go tool covdata textfmt -i=covdatafiles -o=cov.txtgo tool cover -func=cov.txtMerging 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 でテスト結果をマージすると便利だろう。
$ ls covdatafilescovcounters.13326b42c2a107249da22f6e0d35b638.772307.1677775306041466651covcounters.13326b42c2a107249da22f6e0d35b638.772314.1677775306053066987...covcounters.13326b42c2a107249da22f6e0d35b638.774973.1677775310032569308covmeta.13326b42c2a107249da22f6e0d35b638
$ ls covdatafiles | wc 381 381 27401
$ rm -rf merged$ mkdir merged$ go tool covdata merge -i=covdatafiles -o=merged$ ls mergedcovcounters.13326b42c2a107249da22f6e0d35b638.0.1677775331350024014covmeta.13326b42c2a107249da22f6e0d35b638Footnotes
Section titled “Footnotes”-
カバレッジデータのフォーマットや操作はCoverage profiling support for integration testsにまとまっている ↩