フューチャー技術ブログ

Go 1.20 リリース連載 go build に追加される cover オプション(利用例付き)

はじめに

こんにちは。TIG DX ユニット所属、金欠コンサルタントの藤井です。最近でもないですが、SwitchBot ロックと、キーパッド/指紋認証パッドを買いました。我が家における IoT デバイスのカバレッジが着実に向上しており、快適な日々を過ごしています。

Go 1.20 連載 7 記事目にして、最終回の本記事では、go buildコマンドに新たに追加される-coverオプションについてお伝えします。せっかくなので実際に使ってみたレポートもお届けしようと思います。

なお、公式でも詳細な説明を記したランディングページが用意されているので、本記事の後にこちらもご覧いただけると、より一層理解が深まると思います。

https://go.dev/testing/coverage/

cover オプションとは

ビルド・実行手順

go buildコマンドは、作成したアプリケーションをビルドする際に利用しますが、そのオプションに新たに-coverオプションが追加されます。ざっくり書くと、ビルド後のアプリケーションに対し、テストを実行した際のテストカバレッジを取得できるようにするためのオプションです。

使い方は簡単で、単にgo build -cover .のように、いつもの build に-coverオプションを追加するだけです。

このように、-cover以外のオプションをつけずに実行した場合、ローカルのパッケージのみがカバレッジの計測対象となり、(標準含む)外部パッケージは対象外となります。ローカルパッケージの一部をカバレッジ計測対象としたい場合や、外部パッケージも対象としたい場合は、go build -cover -coverpkg=example.com,fmt,net/http .のように、-coverpkgを用いて、明示的に対象パッケージを指定します。-coverpkgを用いた場合は、指定していないパッケージは(ローカル含む)全て対象外となります。

あとはビルドされたバイナリを実行すればよいのですが、その際にGOCOVERDIRの環境変数に、カバレッジを記録したファイルの出力先を設定する必要があります。

また、GOCOVERDIRで指定したディレクトリが存在しない場合は、カバレッジファイルは出力されません。ということで、ディレクトリを作成したうえで、GOCOVERDIR=coverdir ./mainのようにして実行しましょう。すると、指定したディレクトリにそれぞれcovmetacovcountersから始まるファイルが出力されます。covmetaはカバレッジ収集対象のソースコードの各種情報が記録されたファイル、covcountersの方は、カバレッジ等の情報が記録されたファイルです。

前者はmeta-data filesと呼ばれており、何度実行しても(再ビルドされないかぎり)不変な値であるソースコードの情報が記録されているのみのため、初回実行時にのみ作成され、以後更新されません。

一方、 後者はcounter data filesと呼ばれており、こちらは実行の都度変動し得る情報が記録されているため、実行の度に新規で作成されます。

ちなみに、GOCOVERDIR=coverdir go run -cover .のように、go runでももちろん可能です。

カバレッジの確認手順

GOCOVERDIRに出力された 2 種類のファイルはバイナリのため、人間が閲覧できるものにはなっていません。

これを確認するためには、go toolに新たに追加されたサブコマンドcovdataを使用します。

公式そのままの引用ですが、以下のようにpercentサブコマンドで-iオプションにカバレッジファイルの出力先を指定することで、パッケージごとのカバレッジが確認できます。

$ ls somedata
covcounters.c6de772f99010ef5925877a7b05db4cc.2424989.1670252383678349347
covmeta.c6de772f99010ef5925877a7b05db4cc
$ go tool covdata percent -i=somedata
main coverage: 100.0% of statements
mydomain.com/greetings coverage: 100.0% of statements
$

また、textfmtサブコマンドにより、従来のgo testコマンドと同様に、テキスト形式でカバレッジを保存できます。

これも公式の引用ですが、以下のようにgo tool coverに入力することで、go testと同様にカバレッジを確認できます(go tool cover -html=profile.txt -o profile.htmlのように html 形式でのカバレッジ確認も当然可能です)。

$ ls somedata
covcounters.c6de772f99010ef5925877a7b05db4cc.2424989.1670252383678349347
covmeta.c6de772f99010ef5925877a7b05db4cc
$ go tool covdata textfmt -i=somedata -o profile.txt
$ cat profile.txt
mode: set
mydomain.com/myprogram.go:10.13,12.2 1 1
mydomain.com/greetings/greetings.go:3.23,5.2 1 1
$ go tool cover -func=profile.txt
mydomain.com/greetings/greetings.go:3: Goodbye 100.0%
mydomain.com/myprogram.go:10: main 100.0%
total: (statements) 100.0%
$

複数実行時のカバレッジ制御

そのほかにも、go tool covdataにはmerge, subtract, intersectのサブコマンドが存在します。名前の通り、カバレッジを結合差分抽出交差抽出するためのサブコマンドです。

どれも使い方は同じで、go tool covdata merge|subtract|intersect -i=dir1,dir2 -o outputのように使います(merge のみ-iに 3 つ以上のディレクトリを指定できますが、それ以外は 2 つのみ可能です)。

ユースケースとしては以下などでしょうか。

  • merge
    • 異なる環境下での実行結果を結合し、環境依存を吸収した状態でのカバレッジを確認する(公式記載の例)
  • subtract
    • 異なる環境下での実行結果の差分を抽出し、環境依存箇所を特定する
  • intersect
    • 異なる環境下での実行結果の交差部分を抽出し、環境に依存しない箇所を特定する
    • テストケースごとの実行結果の交差部分を抽出し、複数回実行されている(場合によっては無駄であり、テスト効率を下げている)箇所を特定する

パッと思いつく限りではありますが、有用なように見えます。

ほかにも-cpuprofile-memprofileなど、go testでできていたことは大体可能なようです。

cover オプションの利用例

想定されるユースケース

さて、ざっくり概要をさらったところで、この-coverオプションのユースケースについて考えてみます。

proposalには、-coverの導入経緯として、従来のgo testの弱点が以下のように記載されています。

A key weakness of the current implementation is that it does not scale well-- it
is difficult or impossible to gather coverage data for applications as opposed
to collections of packages, and for testing scenarios involving multiple
runs/executions.

ざっくり、従来のgo testはアプリケーション全体のテストや、繰り返し実行されるシナリオに対するテストを弱点としている、といった内容で、なるほど確かにと思う内容です。

導入経緯を踏まえると、ユースケースとしては以下などが挙げられるかなと思います。

  1. DB・外部システム等との結合部分を含む、アプリケーション全体を通してのテスト品質分析
  2. 複数環境下での実行結果差分の解析
  3. 結合・シナリオテストの網羅性分析

REST API サーバを例に、cover を利用したテストを試してみる

ではせっかくなので、-coverを利用したテストを試してみようと思います。

試してみる内容

現在私が携わっている案件では、Go を用いて REST API サーバを複数台構築しています。

システム内のサーバ間通信に加え、外部システムとの通信等が発生することもあり、テストは API に対してリクエストを送り、レスポンス・DB 事後状態を検証する、E2E テストを実施しています(システム全体ではフロントエンドも存在するため、厳密には End to End ではありませんが、API サーバ単独でも公開しているので、E2E と呼んでいます)。実際にテスト対象のサーバをローカル上に建て、別途 Go で書いたテストコードをgo testで実行し、直接テスト対象サーバにリクエストを送っています(他システムはモックサーバや実際のサーバを建てます)。

しかしながら、この方法ではテスト対象サーバのテストカバレッジを取得することはできないため、自動テストのうま味が半減しています。テストの網羅性担保も人力レビューによるものとなってしまっており、かなりつらい状況です。メンバーからもたびたび「カバレッジ取得したいね」「でも E2E だから…」と嘆きの声が上がっています。

というわけで,REST API の E2E テストのカバレッジ取得を試してみます。テスト用のコード(テストコード含む)は以下に配置してあります。

今回のテスト用に突貫で雑に作ったので、このコードに対するツッコミはご容赦ください。

https://github.com/shomuMatch/goCoverTest

ところで、REST API サーバに対して、-coverを用いてテストする際に、一点注意点があります。

それは、カバレッジファイルはプログラムが実行終了した(os.Exit()が呼ばれた・main()が正常にreturnした)タイミングで出力されるということです。つまり、テスト中にpanicを起こして落ちてしまったり、外部から強制終了させてしまうと、カバレッジの取得ができません。今回は特にpanicを起こした場合のことは考えていませんが、テスト終了時に外部から kill させる想定のため、サーバは Graceful にシャットダウンする必要があります。

ここは渋川さんの記事を参考に書きました。
https://future-architect.github.io/articles/20210212/

試してみる

少し話がそれましたが、上記テスト用コードにて、カバレッジ取得を試していきます。

現時点で Go1.20 はリリースされていないため、正式版のgoではなく、gotipを使います。

https://pkg.go.dev/golang.org/dl/gotip

未インストールの方は以下にてインストールいただければと思います。

go install golang.org/dl/gotip@latest
gotip download

それでは、カバレッジファイルの出力先を作成し、-coverをつけてビルド・実行してみましょう。特に普段と変わりなくサーバが立ち上がるはずです。

また、この時点でカバレッジファイルの出力先に meta-data file が出力されているはずです。

$ mkdir coverdir
$ gotip build -cover .
$ GOCOVERDIR=coverdir ./goCoverTest
start receiving at :8888

次に、(上記サーバをバックグラウンドとかコンテナで立てておくか)別のコンソールから、テストコードを実行しましょう(ここは必ずしもgotipである必要はありません)。

ユニットテストコードを一切書いていないので当然ですが、[no test files]になっており、カバレッジが取れていません。

$ gotip test github.com/shomuMatch/goCoverTest/... -cover -count=1
? github.com/shomuMatch/goCoverTest [no test files]
? github.com/shomuMatch/goCoverTest/api [no test files]
? github.com/shomuMatch/goCoverTest/api/path1 [no test files]
? github.com/shomuMatch/goCoverTest/api/path2 [no test files]
ok github.com/shomuMatch/goCoverTest/test/e2e 0.005s coverage: [no statements]

本題はここからです。まずはサーバをシャットダウンしましょう。

シャットダウンが完了したタイミングで、カバレッジファイルの出力先に counter data file が出力されていればここまでは OK です。

ではカバレッジを確認してみましょう。

$ gotip tool covdata percent -i coverdir
github.com/shomuMatch/goCoverTest coverage: 92.9% of statements
github.com/shomuMatch/goCoverTest/api coverage: [no statements]
github.com/shomuMatch/goCoverTest/api/path1 coverage: 90.0% of statements
github.com/shomuMatch/goCoverTest/api/path2 coverage: 88.9% of statements

カバレッジが取れています!!

確認のため、あえて 100%にならないようテストしているのですが、そこも正しく得られていそうです。

せっかくなので html 形式でも確認してみましょう。

gotip tool covdata textfmt -i coverdir -o profile.txt
gotip tool cover -html=profile.txt -o profile.html

上記コマンドで出力された html を表示すると、以下のように通っていない行がハイライトされた状態で見ることができます。

image.png

ということで、無事 REST API サーバの E2E テストのカバレッジ取得に成功しました。

しかも既存のテストの仕組みをほとんど変えることなく対応ができており、実際に案件に導入することも不可能ではなさそうです(Go のバージョンアップ対応は必要ですが)。

おわりに

ということで、Go1.20 で新たに追加されるテストの仕組みである、-coverオプションについて見ながら触ってきました。

当然ですがテストの品質はそのままプロダクトの品質に直結するもののため、こうして仕組みが強化されていくのはとても嬉しいですね。

もう少し頑張ればフロントエンドも含めた E2E テストを全自動で実施し、カバレッジを取得する事もできそうなので、継続して活用していきたいと思います。