Goブログ

Goの統合テストにおけるコードカバレッジ

Than McIntosh
2023年3月8日

コードカバレッジツールは、開発者が特定のテストスイートを実行したときに、ソースコードベースのどの部分が実行(カバー)されるかを判断するのに役立ちます。

Goは以前から、"go test"コマンドの"-cover"フラグを使用して、パッケージレベルでのコードカバレッジを測定するためのサポート(Go 1.2リリースで導入)を提供していました。

このツールはほとんどの場合にうまく機能しますが、大規模なGoアプリケーションにはいくつかの弱点があります。このようなアプリケーションでは、開発者は多くの場合、パッケージレベルのユニットテストに加えて、プログラム全体の動作を検証する「統合」テストを作成します。

このタイプのテストでは、通常、完全なアプリケーションバイナリをビルドし、次に一連の代表的な入力(または、サーバーの場合は本番負荷下)でバイナリを実行して、個々のパッケージを分離してテストするのではなく、すべてのコンポーネントパッケージが一緒に正しく動作することを確認します。

統合テストバイナリは"go test"ではなく"go build"でビルドされるため、これまでGoのツールではこれらのテストのカバレッジプロファイルを収集する簡単な方法がありませんでした。

Go 1.20では、"go build -cover"を使用してカバレッジ計測されたプログラムをビルドし、これらの計測されたバイナリを統合テストにフィードして、カバレッジテストの範囲を拡張できるようになりました。

このブログ記事では、これらの新機能がどのように機能するかを例を挙げて説明し、統合テストからカバレッジプロファイルを収集するためのユースケースとワークフローの概要を示します。

非常に小さなサンプルプログラムを作成し、そのための簡単な統合テストを作成し、次に統合テストからカバレッジプロファイルを収集してみましょう。

この演習では、gitlab.com/golang-commonmark/mdtoolの「mdtool」マークダウン処理ツールを使用します。これは、クライアントがgitlab.com/golang-commonmark/markdown(マークダウンからHTMLへの変換ライブラリ)パッケージをどのように使用できるかを示すように設計されたデモプログラムです。

mdtoolの設定

まず、「mdtool」自体のコピーをダウンロードしましょう(これらの手順を再現可能にするために、特定のバージョンを選択しています)

$ git clone https://gitlab.com/golang-commonmark/mdtool.git
...
$ cd mdtool
$ git tag example e210a4502a825ef7205691395804eefce536a02f
$ git checkout example
...
$

簡単な統合テスト

次に、「mdtool」の簡単な統合テストを作成します。このテストでは、「mdtool」バイナリをビルドし、次に一連の入力マークダウンファイルに対して実行します。この非常に簡単なスクリプトは、テストデータディレクトリの各ファイルに対して「mdtool」バイナリを実行し、何らかの出力が生成され、クラッシュしないことを確認します。

$ cat integration_test.sh
#!/bin/sh
BUILDARGS="$*"
#
# Terminate the test if any command below does not complete successfully.
#
set -e
#
# Download some test inputs (the 'website' repo contains various *.md files).
#
if [ ! -d testdata ]; then
  git clone https://go.googlesource.com/website testdata
  git -C testdata tag example 8bb4a56901ae3b427039d490207a99b48245de2c
  git -C testdata checkout example
fi
#
# Build mdtool binary for testing purposes.
#
rm -f mdtool.exe
go build $BUILDARGS -o mdtool.exe .
#
# Run the tool on a set of input files from 'testdata'.
#
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"
$

以下は、テストの実行例です。

$ /bin/sh integration_test.sh
...
finished processing 380 files, no crashes
$

成功: 「mdtool」バイナリが入力ファイルのセットを正常に処理したことを確認しました。しかし、ツールのソースコードのどの程度が実際に実行されたのでしょうか。次のセクションでは、カバレッジプロファイルを収集して確認します。

統合テストを使用してカバレッジデータを収集する

前のスクリプトを呼び出す別のラッパースクリプトを作成しますが、カバレッジのためにツールをビルドし、結果として得られるプロファイルを後処理します

$ cat wrap_test_for_coverage.sh
#!/bin/sh
set -e
PKGARGS="$*"
#
# Setup
#
rm -rf covdatafiles
mkdir covdatafiles
#
# Pass in "-cover" to the script to build for coverage, then
# run with GOCOVERDIR set.
#
GOCOVERDIR=covdatafiles \
  /bin/sh integration_test.sh -cover $PKGARGS
#
# Post-process the resulting profiles.
#
go tool covdata percent -i=covdatafiles
$

上記のラッパーに関する主な注意点

  • integration_test.shの実行時に"-cover"フラグを渡します。これにより、カバレッジ計測された"mdtool.exe"バイナリが得られます
  • GOCOVERDIR環境変数を、カバレッジデータファイルが書き込まれるディレクトリに設定します
  • テストが完了したら、"go tool covdata percent"を実行して、カバーされたステートメントのパーセンテージに関するレポートを生成します

この新しいラッパースクリプトを実行したときの出力は次のとおりです

$ /bin/sh wrap_test_for_coverage.sh
...
    gitlab.com/golang-commonmark/mdtool coverage: 48.1% of statements
$
# Note: covdatafiles now contains 381 files.

ほら!統合テストが「mdtool」アプリケーションのソースコードを実行する上でどの程度うまく機能しているかを把握できました。

テストハーネスを強化するために変更を加え、2回目のカバレッジ収集実行を行うと、カバレッジレポートに変更が反映されます。たとえば、次の2行をintegration_test.shに追加してテストを改善するとします

./mdtool.exe +ty testdata/README.md  > /dev/null
./mdtool.exe +ta < testdata/README.md  > /dev/null

カバレッジテストラッパーを再度実行します

$ /bin/sh wrap_test_for_coverage.sh
finished processing 380 files, no crashes
    gitlab.com/golang-commonmark/mdtool coverage: 54.6% of statements
$

変更の効果がわかります。ステートメントカバレッジが48%から54%に増加しました。

カバーするパッケージの選択

デフォルトでは、"go build -cover"は、ビルドされているGoモジュールの一部であるパッケージのみを計測します。この場合は、gitlab.com/golang-commonmark/mdtoolパッケージです。ただし、場合によっては、カバレッジ計測を他のパッケージに拡張すると便利な場合があります。これは、"-coverpkg"を"go build -cover"に渡すことで実現できます。

サンプルプログラムの場合、「mdtool」は実際にはパッケージgitlab.com/golang-commonmark/markdownのラッパーにすぎないため、計測されるパッケージのセットにmarkdownを含めることは興味深いことです。

以下は「mdtool」のgo.modファイルです

$ head go.mod
module gitlab.com/golang-commonmark/mdtool

go 1.17

require (
    github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
    gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a
)

"-coverpkg"フラグを使用して、上記の依存関係の1つを含めるために、カバレッジ分析に含めるパッケージを選択できます。以下に例を示します

$ /bin/sh wrap_test_for_coverage.sh -coverpkg=gitlab.com/golang-commonmark/markdown,gitlab.com/golang-commonmark/mdtool
...
    gitlab.com/golang-commonmark/markdown   coverage: 70.6% of statements
    gitlab.com/golang-commonmark/mdtool coverage: 54.6% of statements
$

カバレッジデータファイルの操作

カバレッジ統合テストが完了し、生のデータファイルのセット(この例では、covdatafilesディレクトリの内容)を書き出した場合、これらのファイルをさまざまな方法で後処理できます。

プロファイルを'-coverprofile'テキスト形式に変換する

ユニットテストを操作する場合、go test -coverprofile=abc.txtを実行して、特定のカバレッジテスト実行のテキスト形式のカバレッジプロファイルを書き込むことができます。

go build -coverでビルドされたバイナリでは、GOCOVERDIRディレクトリに出力されたファイルに対してgo tool covdata textfmtを実行することで、後からテキスト形式のプロファイルを生成できます。

このステップが完了したら、go tool cover -func=<file>またはgo tool cover -html=<file>を使用して、go test -coverprofileと同じようにデータを解釈/視覚化できます。

$ /bin/sh wrap_test_for_coverage.sh
...
$ go tool covdata textfmt -i=covdatafiles -o=cov.txt
$ go tool cover -func=cov.txt
gitlab.com/golang-commonmark/mdtool/main.go:40:     readFromStdin   100.0%
gitlab.com/golang-commonmark/mdtool/main.go:44:     readFromFile    80.0%
gitlab.com/golang-commonmark/mdtool/main.go:54:     readFromWeb 0.0%
gitlab.com/golang-commonmark/mdtool/main.go:64:     readInput   80.0%
gitlab.com/golang-commonmark/mdtool/main.go:74:     extractText 100.0%
gitlab.com/golang-commonmark/mdtool/main.go:88:     writePreamble   100.0%
gitlab.com/golang-commonmark/mdtool/main.go:111:    writePostamble  100.0%
gitlab.com/golang-commonmark/mdtool/main.go:118:    handler     0.0%
gitlab.com/golang-commonmark/mdtool/main.go:139:    main        51.6%
total:                          (statements)    54.6%
$

生のプロファイルを「go tool covdata merge」でマージする

"-cover"ビルドされたアプリケーションを実行するたびに、GOCOVERDIR環境変数で指定されたディレクトリに1つ以上のデータファイルが書き込まれます。統合テストでN個のプログラムが実行される場合、出力ディレクトリにはO(N)個のファイルができます。データファイルには通常、重複したコンテンツが多数含まれているため、データを圧縮したり、異なる統合テストの実行からデータセットを結合したりするために、go tool covdata mergeコマンドを使用してプロファイルをマージできます。例

$ /bin/sh wrap_test_for_coverage.sh
finished processing 380 files, no crashes
    gitlab.com/golang-commonmark/mdtool coverage: 54.6% of statements
$ 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
$

go tool covdata merge操作では、必要に応じて、特定のパッケージまたはパッケージのセットを選択するために使用できる-pkgフラグも受け入れます。

このマージ機能は、他のテストハーネスによって生成された実行を含め、さまざまなタイプのテスト実行からの結果を組み合わせるのにも役立ちます。

まとめ

これで完了です。1.20リリースでは、Goのカバレッジツールはパッケージテストに限定されなくなり、より大規模な統合テストからプロファイルを収集することをサポートします。大規模で複雑なテストがどの程度機能しているか、およびソースコードのどの部分を実行しているかを理解するのに、新機能を活用していただければ幸いです。

これらの新機能を試していただき、問題が発生した場合は、GitHubのIssueトラッカーに問題を報告してください。ありがとうございます。

次の記事:Go開発者アンケート2023年第1四半期の結果
前の記事:すべての比較可能な型
ブログインデックス