The Go Blog
Go 統合テストのコードカバレッジ
コードカバレッジツールは、開発者が特定のテストスイートを実行したときに、ソースコードベースのどの部分が実行 (カバー) されるかを判断するのに役立ちます。
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」マークダウン処理ツールを使用します。これは、クライアントがマークダウンから HTML への変換ライブラリであるパッケージ gitlab.com/golang-commonmark/markdown をどのように使用できるかを示すために設計されたデモプログラムです。
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.
Voilà!これで、統合テストが「mdtool」アプリケーションのソースコードをどれだけうまく実行しているかについて、ある程度のアイデアが得られました。
テストハーネスを強化するために変更を加え、2回目のカバレッジ収集を実行すると、その変更がカバレッジレポートに反映されます。たとえば、integration_test.sh にこれら2つの追加行を追加してテストを改善するとします。
./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 パッケージです。ただし、場合によっては、カバレッジ計測を他のパッケージに拡張すると便利です。これは、「go build -cover」に「-coverpkg」を渡すことで実現できます。
私たちのサンプルプログラム「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」フラグを使用して、上記の中からいずれかの依存関係を含めるためにカバレッジ分析に含めるパッケージを選択できます。以下に例を示します。
$ /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 フラグも受け入れます。
このマージ機能は、他のテストハーネスによって生成された実行を含む、異なる種類のテスト実行からの結果を結合するためにも役立ちます。
まとめ
これで終わりです。Go 1.20 リリースにより、Go のカバレッジツールはパッケージテストに限定されなくなり、より大規模な統合テストからのプロファイル収集をサポートするようになりました。皆様がこの新機能をうまく活用して、大規模で複雑なテストがどれだけうまく機能しているか、そしてソースコードのどの部分を実行しているかを理解するのに役立つことを願っています。
これらの新機能をぜひお試しください。そして、何か問題が発生した場合は、いつものように GitHub の Issue Tracker に課題を提出してください。ありがとうございます。
次の記事: Go Developer Survey 2023 Q1 結果
前の記事: すべての比較可能な型
ブログインデックス