every Tech Blog

株式会社エブリーのTech Blogです。

go1.22 からのテストカバレッジとの付き合い方

はじめに

こんにちは、トモニテ開発部ソフトウェアエンジニア兼、CTO 室 Dev Enable グループの rymiyamoto です。
最近はエルデンリングが再燃しています。

2024/06/18 に Go Conference 2024 の非公式イベントとして の Go BASH が開催されました。

andpad.connpass.com

その際に go の ver 1.22 におけるテストカバレッジについて話をしてきたため、その内容を記事にまとめたものです。

speakerdeck.com

そもそも話題の背景

go1.22 が出たタイミンで影響少なそうなリポジトリで試しにアップデートしてみたところ、CI を眺めているとカバレッジが大きく低下しているのを発見しました。

一瞬 go test -cover 壊れたのかと思いましたが、そうではなくリリースノートにも記載されている通り、カバレッジの計算方法が変わったことが原因でした。

tip.golang.org

内容をまとめると

  • Go 1.22 以降、テストファイルのないパッケージでもカバレッジが表示される
  • 関数がカバーされていない場合は 0.0%扱い
  • 実行可能なコードが全くない場合は、「テストファイルがない」と報告される
    • ex.) 構造体やインターフェースの定義のみで関数がない場合など

のように、テストカバレッジの状況をより詳細に把握できるようになりました。

低下の要因たち

テストを行っていない部分が影響を及ぼしており、それらを以下に列挙します。

  • 自動生成系のもの
    • エンドポイント生成(OpenAPI, GraphQL のジェネレータ)
    • ORM
  • 意図的に書いてない実装
    • テスト内容がテストコード作成時の労力に見合ってないもの
    • 必要なテスト環境が複雑でチームやプロジェクト規模によっては Skip してるもの

自動生成系のものに関しては、提供されている pkg によってはテストコードの生成までしてくれるものもありますが、それでも全てのケースを網羅するのは難しいです。
また、意図的に書いてない実装は単純にテストをちゃんと書けばいいだけではありますが、プロダクトのフェーズやチームのレベル感次第となります。

一時しのぎとして 1.21 以前と同じ挙動にする場合は、テスト実行時に GOEXPERIMENT = nocoverageredesign を設定することで可能です。

GOEXPERIMENT=nocoverageredesign go test -cover ./...

ただし、GOEXPERIMENT を指定すると予告なく使用できなくなる可能性があるため、別の対応策を考える必要があります。

仕様変更に至るまでの経緯

ここまでの話を踏まえて、なぜこのような仕様変更が行われたのか気になり、その原因となった issue を探してみました。

github.com

概要としてはカバレッジレポートの出力方式について改善の余地あるのでは?というものです。
議論自体は 2018/03/18 から行われており、当時は go1.10 時代です。

go1.10 のリリースノートにテストに関する変更が記載されており、その中で一度に複数パッケージのカバレッジが取れるようになったことが記載されています。

go.dev

この変更の際に、カバレッジを計測できないテストがないものは、go1.21 と同じように計上されていないままでした。 そのため、カバレッジは計測範囲に対しての全体の割合を出したいという要望が出てきたようです。

中では以下のことが議論がされていました。

  • テストファイルのないパッケージについてカバレッジ(0%)を出力するべきか
    • 出力した方が未テストの関数があることが分かりやすい
    • 出力しないと総合カバレッジ率が不自然に下がる
  • 総合カバレッジにテストファイルのないパッケージを含めるべきか
    • 含めた方が未テストの関数が明確になる
  • go test -cover と go test -coverprofile でカバレッジ出力を統一するべきか
    • 統一しないとユーザーに混乱を招く可能性
    • 統一すると要件によっては不自然な動作になる可能性
  • テストファイルがない=未テストなのか、それとも単にテストが書かれていないだけなのか
    • 未テストと見なすべきか
    • コードに応じて判断が分かれる可能性
  • [no test files]と 0.0%のどちらがユーザーにとってわかりやすいか

決定的な決め手はなかったようですが、やはり明示的に引数で対象とするパッケージを指定している以上レポートに含まれるべきという最初の要望が最終的に採用されました。

上記の話を踏まえて、以下の方針で計測方法を見直すことにしました。

  • 意図的にテストないものをカバレッジ入れたくはない
    • 注視したいのは自分たちが実装している部分
      • ビジネスロジック周り
    • そもそも不要なものまで計測対象にしているのが良くない
      • 自動生成系、意図的に書いてないところ
  • 引数で対象 pkg は指定して、入れたくないものは除外しておく
    • go test -cover ./… → go test -cover pkg1 pkg2

計測方法の見直し

基本方針をもとにホワイトリスト型とブラックリスト型の 2 つの方法を試してみました。

テストファイルがあるものだけ抽出する(ホワイトリスト型)

専用の抽出スクリプトから絞り込むようにしたものです。
この際 *_test.go があるディレクトリが対象とすれば自動生成系は巻き込まれません

  • メリット
    • これまでと同じような挙動に出来る
  • デメリット
    • コード全体の質を測るうえではテストがないと隠蔽されてしまう

listup_test_pkg.sh

#!/bin/bash

# 現在のディレクトリからすべてのGoパッケージを検索
for pkg in $(go list ./...); do
  # 各パッケージのディレクトリを取得
  pkg_dir=$(go list -f '{{.Dir}}' $pkg)
  # *_test.goファイルがそのディレクトリに存在するか確認
  if [[ $(ls $pkg_dir/*_test.go 2> /dev/null) ]]; then
    # 存在する場合はパッケージ名を表示
    echo $pkg
  fi
done
go test -cover $(./listup_test_pkg.sh)

除外したい pkg を名指しする(ブラックリスト型)

除外対象 pkg を一元管理して、テスト時に除外するようにしました。
管理は何かしらのファイルでできればいいので、yaml や json 等で記載します。
書き方によってはファイル名やディレクトリ構成に規則性があれば管理は容易となります (他の手として go test -cover の結果を grep -v で引き抜く手もありますが余計なテスト実行となるので採用しませんでした)

  • メリット
    • 除外したいものが明示的
    • 今回の変更の背景を考えると現実的な落とし所になりそう
  • デメリット
    • 除外箇所の羅列が面倒で手間がかかる

testcnf.yml

ignore:
  - github.com/rymiyamoto/example-api/internal/app/ignore1
  - github.com/rymiyamoto/example-api/internal/dashboard/ignore2
  - github.com/rymiyamoto/example-api/internal/external/ignore3
exclude_patterns=$(yq e '.ignore[]' testcnf.yml | sed 's/^/-e /' | tr '\n' ' ')
go test -cover $(go list ./... | grep -vE $(echo $exclude_patterns))

まとめ

ここまで go1.22 移行後のテストカバレッジについて話してきましたが、その取り扱い方はプロダクトやチームによって異なると思います。
今回のように仕様変更による影響がある場合は、その変更の背景を知ることで今後の方針を決めるのに役立つはずです。

また、テストカバレッジはあくまで一つの指標であり、それが全てではないことを忘れずに、テストの質やカバレッジの意味を考えることが大切だと感じました。
今後も go のアップデートに合わせてプロダクトの品質向上に努めていきたいと思います。