はじめに
テストでコード品質を担保していくことは、継続的インテグレーションの観点などで必要不可欠です。そして、十分なテストコードが書かれているかどうかの指標として、よく使われるものといえばテストカバレッジがあります。
Goではgo test
コマンドと、go tool cover
コマンドがカバレッジ計測の機能を担っています。今回は、これらのツールをより深く使い込んでいくために、既存機能の一歩進んだ使い方や最新機能について紹介します。
なお、本記事で紹介しているコマンドなどはmacOSで実行した場合の例となります。
オリジナルのカバレッジ統計データを集計する
まずは既存のカバレッジの統計データを取得する方法を振り返り、より詳細な情報を集計するアプローチについて紹介します。
Goのカバレッジツールで出力できる統計データ
既存のgo test
コマンドおよびgo tool cover
コマンドで出力できるカバレッジの統計データは次の3種類です。
- パッケージ単位のカバレッジ
(%) - 関数ごとのカバレッジ
(%) - 全体のカバレッジ
(%)
それぞれ次のコマンドを実行することで確認できます。
# 1 $ go test -cover ./... # 2, 3 $ go test -coverprofile=./coverage.txt ./... $ go tool cover -func coverage.txt
これらの方法で最低限の情報は得られます。しかし、アプリケーションの規模が大きくなってくると違った観点の情報もほしくなってきます。たとえば
そこで次に、go tool cover
コマンドにはない形式でカバレッジ情報を出力する方法を紹介します。
-coverprofile
オプションで出力されるレポート形式
鍵となるのが、go test
コマンドの-coverprofile
オプションを指定して出力されるレポートです。このオプションを使うと、次のような形式でファイル出力されます。
mode: set
sample/a/a.go:3.17,5.2 1 1
sample/a/aa/aa.go:3.29,4.18 1 1
sample/a/aa/aa.go:7.2,7.14 1 1
sample/a/aa/aa.go:4.18,6.3 1 0
一見不可思議な数値が並んでいますが、それぞれ意味は次のとおりです。2つ目の開始行から5つ目の終了列はコードブロックに関してです。
{ファイルパス}:{開始行}.{開始列},{終了行}.{終了列} {ブロックの行数} {テスト通過回数}
なお、このことはcoverパッケージのparseLine
関数からわかります。
このレポート情報と元のコードを照らしあわせると次のようになります。
これらの情報を踏まえて詳細を読み取っていきます。具体的には次のことがわかります。
- aa.
goのcode blockの総行数は3行 - aa.
goのテストされているcode blockの行数は2行で、カバレッジは66. 7% (2/ 3) - 結果がfalseになるテストがある
- 結果がtrueになるテストはない
大切なのは、-coverprofile
オプションで出力されるレポートにはファイルパスとコードブロックごとのテスト情報が含まれていることです。tool cover
にはない形式でカバレッジ情報を出力したい場合は、このデータをパースした後、それぞれの目的に合わせて集計することになります。
ディレクトリ単位で行数を含めて集計する
この-coverprofile
オプションで出力されるレポートを活用して、オリジナルのカバレッジ統計データを集計する具体例を紹介します。
筆者の所属するナレッジワークでテストを拡充する計画をした際、それを細かくブレイクダウンするために、ディレクトリ単位のカバレッジを求めて判断材料に使いました。このケースを例に挙げて進めます。
ここでは仮に、全体のカバレッジを2%上げるという目標を立てたとします。目標達成のために何個テストを追加すればよいのか、そのためにどれくらいの工数が必要なのかを見積もらなければなりません。
しかし、標準で用意されている関数とパッケージ単位のカバレッジデータだけでは情報が足りないと判断しました。たとえば、ある関数のカバレッジが0%だった場合、それを100%にすると全体のカバレッジがどのくらいに増えるかは、コード全体の行数および対象とする関数の行数がわからなければ計算できません。
また、パッケージ数が多い場合、複数のパッケージをまとめたディレクトリ単位でカバレッジを集計すると全体感が把握しやすくなります。
集計処理は次の4ステップで構成できます。とてもシンプルです。
- レポートファイルをパースしてGoファイルのパス、ブロックごとの行数およびテスト回数を取得する
- ファイルに含まれるブロックの行数およびテスト済みの行数の和を集計する
- ファイルパスから木構造を組み立てる
- 葉(ファイル)
から根 (ルートディレクトリ) にむかって全体行数およびテスト済みの行数を集計する
この集計処理の実装例はリポジトリ上で公開しています。
なお前項のレポートファイルの例だと、次の図のような集計結果になります。
ここで紹介した集計以外にも、ブロック数を集計すればコードの分岐処理の数がわかるため、関数やファイルごとのおよその複雑さを把握できます。またカバレッジレポートにはブロックの行・
このように、標準のカバレッジ情報で物足りない場合は、自身で集計できないか是非検討してみてください。
統合テストでカバレッジを計測する
続いて、統合テストにおけるカバレッジ計測について紹介します。
Go 1.go test
コマンドで実行した時のカバレッジしか集計できませんでした。たとえば、APIの自動テストを構築したとしても、APIテストでサーバーのソースコードをどの程度網羅できているかを計測することはできません。
こういった課題に対して、2023年2月にリリースされたGo 1.
go build
コマンドの-cover
オプション
その追加された機能がgo build
コマンドにおける、カバレッジ集計用のオプション-cover
)
go test
コマンドとgo tool cover
コマンドを使ったGo 1.
まずは基本的な使い方を見てみましょう。
最初に-cover
オプションを指定してビルドします。その後、カバレッジレポート保存用のディレクトリを環境変数GOCOVERDIR
で指定し、バイナリを実行します。
$ go build -cover $ mkdir _coverage $ GOCOVERDIR=_coverage ./main
実行すると指定したディレクトリにカバレッジレポートが出力されます。
$ ls _coverage covcounters.b8766fa017dad81194d2a53bce9517fa.50700.1676182425749415000 covmeta.b8766fa017dad81194d2a53bce9517fa
出力されたファイルのうち、covcountersという名前で始まるファイルがカバレッジ情報です。もう一方のcovmetaで始まるファイルは、ソースコードに関するメタデータで、ファイル名などが含まれます。
どちらのファイルもバイナリデータであるため、解析するためにはGo 1.go tool covdata
コマンドを使います。
go tool cover
で集計できるパッケージごとの集計や関数ごとの集計が可能で、go test
コマンドの-coverprofile
オプションで出力される従来のテキスト形式への変換もサポートされています。
# パッケージ単位のカバレッジ $ go tool covdata percent -i=_coverage main coverage: 83.3% of statements # 関数単位のカバレッジ $ go tool covdata func -i=_coverage sample/main.go:9: main 83.3% total (statements) 83.3% # テキスト形式への変換 $ go tool covdata textfmt -i=coverage -o coverage.txt $ cat coverage.txt mode: set main/main.go:9.13,14.16 5 1 main/main.go:14.16,16.3 1 0
また、これら以外にも複数の実行結果のマージや差分取得の機能も用意されています。詳しくは公式ドキュメントをご覧ください。
このように、ビルド時に-cover
オプションを指定するだけで、生成されたバイナリを実行することでカバレッジを簡単に取得できます。しかし、一つ大きな制約があります。
それは、実行が正常に終了した場合のみ、カバレッジデータが出力される点です。たとえば、APIサーバーの統合テストのカバレッジを取得しようとしても、サーバーが動き続けている限りはカバレッジデータは出力されません。また、強制終了させてもカバレッジデータは失われます。
この制約に対して、具体的な対策が公式に用意されています。
runtime/coverageパッケージ
正常終了するプログラムしかカバレッジレポートが出力されない制約への対策には、同じくGo 1.
runtime/
ここで、次のような3つのAPI
まずこのコードを-cover
オプションをつけてビルドし、実行します。
$ go build -cover $ GOCOVERDIR=coverage ./main
そして別のシェルを起動し、curl
コマンドを用いてサーバーに対して次のようにhttp://
とhttp://
にそれぞれリクエストを送ります。そうすると、サーバーは起動したままで、coverageディレクトリにレポートが出力されます。
$ curl http://localhost:8080/hello Hello World $ curl http://localhost:8080/record $ ls coverage covcounters.b8766fa017dad81194d2a53bce9517fa.59654.1676187445218891000 covmeta.b8766fa017dad81194d2a53bce9517fa
レポートを解析すると、sample/
となっており、3つのAPIのうち、HelloWorld2だけがテストされていないことが確認できます。
$ go tool covdata textfmt -i=coverage -o coverage.txt $ cat coverage.txt mode: set sample/main.go:9.13,14.16 5 1 sample/main.go:14.16,16.3 1 0 sample/api/api.go:9.57,11.2 1 1 sample/api/api.go:13.58,15.2 1 0 sample/api/api.go:17.53,19.2 1 1
このように、runtime/coverage.
関数は、io.
型を引数に取れるため、好きな場所に出力できます。そのため、クラウドストレージに結果を出力したい場合も簡単に実装できます。
前述したように、出力されるバイナリ形式のレポートファイルは複数の結果をマージすることも可能です。テストを並列実行してそれぞれレポートを出力したあとにマージするといったアプローチも取れるでしょう。
ここでは簡単な例として、レポート出力用のAPIを別で定義しましたが、レポート出力用のインターセプターやミドルウェアをハンドラに追加して、API実行の度にレポートを出力するといった方法を取ることもできます。
APIの自動テストと組み合わせてカバレッジを計測できるようになったのは素晴らしいです。
おわりに
本記事では次のようなことを紹介しました。
go test -coverprofile
コマンドで出力されるレポートには、ファイルパス・コードのブロック情報 (行、列、行数)、テスト実行情報が含まれている - プロファイルレポートをパースして集計することで、
go tool cover
コマンドにはない独自の集計が可能になる - Go 1.
20で go build
コマンドに-cover
オプションが追加され、バイナリ実行時にカバレッジを計測できるようになった - バイナリ実行時のカバレッジ計測では正常終了する必要があるため、異常終了するコードや終了しないサーバのコードの場合は、runtime/
coverageパッケージを使って実行途中でレポートを出力する
コードの健全性を保つには、何かしらの方法でコードの健全性を定量化し、その指標を計測し続ける必要があります。健全性の指標としてテストのカバレッジは銀の弾丸ではないものの、少なくとも現実的な解決策の一つだと言えるでしょう。
開発を進める中で、カバレッジが少しずつ減っていくような傾向がみられたとしたら、
カバレッジの計測はこのような悪い兆候に気づくための手段の一つです。加えてカバレッジを計測すること自体がテストコードを書くモチベーションにも繋がります。ぜひみなさんの環境にあった適切なカバレッジを計測して、健全な開発の一助としていただければと思います。