Goのパッケージ横断でのテストカバレッジを計測して可視化した話 Now In REALITY Tech #127
こんにちは、サーバエンジニアの abe です。
REALITY では内部品質の向上というテーマで自動テストまわりの整備・改善を進めており、その中でテストカバレッジの計測・可視化に取り組みました。その際 Go のテストカバレッジ計測についていろいろ試行錯誤したので、今回はその話をしようと思います。
Go のカバレッジ計測
REALITY の API サーバで使用している Go 言語では、標準機能の go test コマンドでテストカバレッジの計測がサポートされています。ここでは model パッケージと service パッケージのカバレッジを出してみましょう。
$ go test ./model ./service -covermode=count -coverprofile=cover.out
ok model 0.385s coverage: 23.6% of statements
ok service 8.390s coverage: 41.8% of statements
パッケージごとにテストを実行し、通過した命令文の割合がカバレッジとして算出されています。
ここで気になるのは、カバレッジの計測単位は基本的にパッケージごととなっているため、service の関数から model の関数を呼んでも model のカバレッジには含まれないことです。
package model
type User struct {
UserID string
Name string
}
type Users []*User
func (u Users) UserIDs() []string {
ids := make([]string, 0, len(u))
for _, user := range u {
ids = append(ids, user.UserID)
}
return ids
}
例えばこの User のようなモデルに対し UserIDs() 的な関数を書く機会は多いと思いますが、似たようなモデルを定義する際に毎回 UserIDs() のテストを書くのは大変ですし、品質向上への効果も限定的です。
package service
type UserService struct {}
func (s *UserService) GetUserActivities(users Users) (UserActivities, error) {
userIDs := users.UserIDs()
// users, userIDs を使った後続の処理
return userActivities, nil
}
service 側のこの GetUserActivities() 関数に適切にテストが書かれていれば Users.UserIDs() についても最低限の検証は行われているので、この関数のテストによる呼び出しも model 側のカバレッジに含められれば、より実態としての「自動テストされているコードの割合」を計測できそうです。
ということで今回のカバレッジ計測では、service のテストで model の関数を通ったなら、その分も model のカバレッジに含めたいと考えました。
パッケージ横断でのカバレッジ
パッケージを横断したカバレッジの計測ができないか調べていたところ、 coverpkg というそれっぽいオプションを発見しました。このオプションについては go help testflag コマンドで確認できます (go help test に書いてほしい…)
$ go help testflag
(抜粋)
-coverpkg pattern1,pattern2,pattern3
Apply coverage analysis in each test to packages matching the patterns.
The default is for each test to analyze only the package being tested.
See 'go help packages' for a description of package patterns.
Sets -cover.
さっそく実行してみます。
$ go test ./model ./service -covermode=count -coverpkg=./model,./service
ok model 0.392s coverage: 2.8% of statements in ./model, ./service
ok service 7.842s coverage: 40.2% of statements in ./model, ./service
あれ、さっきと比べて model の数値が大幅に下がっていますね 🤔
プロファイルを眺めて調べた感じだと、このカバレッジは例えば model であれば「model のテストで通過した model と service のコードの割合」として計算されてそうな気配でした。
REALITY のアーキテクチャでは model が service を呼ぶことは基本的にはないため、service のカバレッジはあまり変わっていないのに対し model のカバレッジが低くなっていたようです。
コード全体のうち対象パッケージのテストでカバーできている割合を見るという観点ではこれでも良いのですが、今回は「テスト全体で対象パッケージのコードがどれくらいカバーされているか」を計算するのが目標です。
model パッケージであれば、「model と service のテストで通過した model のコードの割合」を計算したいところです。
カバレッジプロファイルの解析
そこで今回は、カバレッジプロファイルを cover パッケージを使って解析することで欲しい数値を計算しました。coverprofile によって出力されたカバレッジプロファイルを cover.ParseProfiles 関数を使ってパースすると、Profile 構造体に情報が詰められて返ってきます。
// []*Profile が返ってくる
profiles, err := cover.ParseProfiles(coverFile)
type Profile struct {
FileName string
Mode string
Blocks []ProfileBlock
}
type ProfileBlock struct {
StartLine, StartCol int
EndLine, EndCol int
NumStmt, Count int
}
この Profile はファイルごと・ブロックごとにまとまっており、ブロックを通過した回数が ProfileBlock.Count に入っています。
プロファイル生成時に coverpkg を指定しなかった場合は各ファイルが属するパッケージのテストで通過した分しか Count に含まれませんが、coverpkg を指定すればパッケージをまたいだ呼び出しも Count に含まれるので、パッケージを横断した解析ができます。
FileName を見てパッケージごとに分類し、Count > 0 の NumStmt の和を全体の NumStmt の和で割ることで、欲しい数値を計算できそうです。
プロファイルを解析することで、パッケージ単位だけではなくマイクロサービスごとのトータルカバレッジ、マイクロサービス横断でのトータルカバレッジなどさまざまな粒度のカバレッジが簡単に出せるようになりました。
トータルカバレッジは go tool cover でも出力は可能ですが、自分で計算することで一部パッケージを計測から除外するなども柔軟に対応しやすくなっています。
20241030,model,47.19
20241030,service,41.81
20241030,total,42.45
可視化する際に使えるよう、日付を付けて CSV 形式でカバレッジを出力します。model のコードのうち model, service の自動テストで通過している割合は約 47% でした。
ダッシュボードの作成
このカバレッジ計算ツールでさまざまな粒度のカバレッジを daily で出力し、Looker Studio 上にダッシュボードを作成しました。
これはマイクロサービスごとのカバレッジを時系列で表示したものです。サービスによって多少の上下はありますが、全体としては上がってきていますね! まだカバレッジが低いサービスもありますが、そこにテストを足してリリースすると一気に数値が上がったりして面白いです。
中央付近にある太い点線が計測対象のマイクロサービスを横断したトータルの数値で、Go サーバ全体のカバレッジ的なものです。全体だとかなりのコード量があるため少しずつではありますが、着実に上がってきています。
このダッシュボードを定例で確認し、カバレッジが向上した際にはどのリリースで改善したのかなどワイワイすることで、テスト書いていこうという雰囲気を作っています。
カバレッジが低いサービスにテストを足していくというよりは、普段の開発で触ったところにきちんとテストを書くことを意識していますが、ちゃんとカバレッジが上がっていることがわかって良いなと。
自動テストへの取り組み
今回はカバレッジの計測・可視化の取り組みについて紹介しました。
自動テストへの取り組みはこれだけではなく、テストを書きやすくするための環境整備、テストの書き方やルールをまとめたガイドラインの策定など様々な取り組みを行っています。
これらの取り組みを通して、チーム全体に「ちゃんとテストを書こう」という意識や文化が少しずつ根付いてきているように感じます。これからも快適に安全なコードを開発していけるよう、引き続きテストの推進・整備に取り組んでいきます!