はじめに
こんにちは。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
のようにして実行しましょう。すると、指定したディレクトリにそれぞれcovmeta
とcovcounters
から始まるファイルが出力されます。covmeta
はカバレッジ収集対象のソースコードの各種情報が記録されたファイル、covcounters
の方は、カバレッジ等の情報が記録されたファイルです。
前者はmeta-data files
と呼ばれており、何度実行しても(再ビルドされないかぎり)不変な値であるソースコードの情報が記録されているのみのため、初回実行時にのみ作成され、以後更新されません。
一方、 後者はcounter data files
と呼ばれており、こちらは実行の都度変動し得る情報が記録されているため、実行の度に新規で作成されます。
ちなみに、GOCOVERDIR=coverdir go run -cover .
のように、go run
でももちろん可能です。
カバレッジの確認手順
GOCOVERDIR
に出力された 2 種類のファイルはバイナリのため、人間が閲覧できるものにはなっていません。
これを確認するためには、go tool
に新たに追加されたサブコマンドcovdata
を使用します。
公式そのままの引用ですが、以下のようにpercent
サブコマンドで-i
オプションにカバレッジファイルの出力先を指定することで、パッケージごとのカバレッジが確認できます。
$ ls somedata |
また、textfmt
サブコマンドにより、従来のgo test
コマンドと同様に、テキスト形式でカバレッジを保存できます。
これも公式の引用ですが、以下のようにgo tool cover
に入力することで、go test
と同様にカバレッジを確認できます(go tool cover -html=profile.txt -o profile.html
のように html 形式でのカバレッジ確認も当然可能です)。
$ ls somedata |
複数実行時のカバレッジ制御
そのほかにも、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 |
ざっくり、従来のgo test
はアプリケーション全体のテストや、繰り返し実行されるシナリオに対するテストを弱点としている、といった内容で、なるほど確かにと思う内容です。
導入経緯を踏まえると、ユースケースとしては以下などが挙げられるかなと思います。
- DB・外部システム等との結合部分を含む、アプリケーション全体を通してのテスト品質分析
- 複数環境下での実行結果差分の解析
- 結合・シナリオテストの網羅性分析
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 |
それでは、カバレッジファイルの出力先を作成し、-cover
をつけてビルド・実行してみましょう。特に普段と変わりなくサーバが立ち上がるはずです。
また、この時点でカバレッジファイルの出力先に meta-data file が出力されているはずです。
$ mkdir coverdir |
次に、(上記サーバをバックグラウンドとかコンテナで立てておくか)別のコンソールから、テストコードを実行しましょう(ここは必ずしもgotip
である必要はありません)。
ユニットテストコードを一切書いていないので当然ですが、[no test files]
になっており、カバレッジが取れていません。
$ gotip test github.com/shomuMatch/goCoverTest/... -cover -count=1 |
本題はここからです。まずはサーバをシャットダウンしましょう。
シャットダウンが完了したタイミングで、カバレッジファイルの出力先に counter data file が出力されていればここまでは OK です。
ではカバレッジを確認してみましょう。
$ gotip tool covdata percent -i coverdir |
カバレッジが取れています!!
確認のため、あえて 100%にならないようテストしているのですが、そこも正しく得られていそうです。
せっかくなので html 形式でも確認してみましょう。
gotip tool covdata textfmt -i coverdir -o profile.txt |
上記コマンドで出力された html を表示すると、以下のように通っていない行がハイライトされた状態で見ることができます。
ということで、無事 REST API サーバの E2E テストのカバレッジ取得に成功しました。
しかも既存のテストの仕組みをほとんど変えることなく対応ができており、実際に案件に導入することも不可能ではなさそうです(Go のバージョンアップ対応は必要ですが)。
おわりに
ということで、Go1.20 で新たに追加されるテストの仕組みである、-cover
オプションについて見ながら触ってきました。
当然ですがテストの品質はそのままプロダクトの品質に直結するもののため、こうして仕組みが強化されていくのはとても嬉しいですね。
もう少し頑張ればフロントエンドも含めた E2E テストを全自動で実施し、カバレッジを取得する事もできそうなので、継続して活用していきたいと思います。