
こんにちは。よっしーです(^^)
本日は、Go言語の.統合テストのカバレッジついて解説しています。
背景
Go言語でテストを書いていると、「go test -coverprofileでカバレッジが取れるのは知っているけど、統合テストのカバレッジってどうやって測るんだろう?」と疑問に思ったことはありませんか?
公式ドキュメントには、Go 1.20から統合テストのカバレッジ測定がサポートされたことが書かれていますが、英語で書かれている上に、ユニットテストとの違いや具体的な手順の説明が簡潔すぎて、初めて読むと「結局どうすればいいの?」と戸惑ってしまうかもしれません。
この記事では、公式ドキュメントの内容を丁寧な日本語に翻訳し、さらに初心者の方でも理解できるように、ユニットテストと統合テストの違い、なぜ3ステップ必要なのか、そして実際にどのようなコマンドを実行すればよいのかを、具体例を交えて解説していきます。
統合テストのカバレッジ測定は一見難しそうに見えますが、仕組みを理解すれば決して複雑ではありません。実際のアプリケーションでどれだけコードがテストされているかを把握することで、より品質の高いソフトウェア開発ができるようになります。一緒に学んでいきましょう!
複数回実行を伴うテスト
統合テストは、多くの場合、複数回のプログラム実行を伴います。プログラムが “-cover” でビルドされている場合、各実行ごとに新しいデータファイルが生成されます。例:
$ mkdir somedata2
$ GOCOVERDIR=somedata2 ./myprogram.exe // 1回目の実行
I say "Hello, world." and "see ya"
$ GOCOVERDIR=somedata2 ./myprogram.exe -flag // 2回目の実行
I say "Hello, world." and "see ya"
$ ls somedata2
covcounters.890814fca98ac3a4d41b9bd2a7ec9f7f.2456041.1670259309405583534
covcounters.890814fca98ac3a4d41b9bd2a7ec9f7f.2456047.1670259309410891043
covmeta.890814fca98ac3a4d41b9bd2a7ec9f7f
$
カバレッジデータ出力ファイルには2つの種類があります。メタデータファイル(ソースファイル名や関数名など、実行ごとに変わらない項目を含む)と、カウンターデータファイル(プログラムの実行された部分を記録する)です。
上記の例では、1回目の実行で2つのファイル(カウンターとメタデータ)が生成されましたが、2回目の実行ではカウンターデータファイルのみが生成されました。メタデータは実行ごとに変わらないため、一度だけ書き込めば良いからです。
解説
統合テストの複数回実行とは
統合テストでは、同じプログラムを異なる条件で何度も実行することがよくあります。
ECサイトのテスト例:
# シナリオ1: 商品閲覧だけ
./shop browse
# シナリオ2: カートに追加
./shop add-to-cart
# シナリオ3: 購入
./shop checkout
# シナリオ4: レビュー投稿
./shop review
それぞれの実行で、プログラムの違う部分が実行されます。すべてのシナリオを合わせることで、全体のカバレッジがわかります。
複数回実行の基本パターン
# 準備: カバレッジデータ保存用ディレクトリを作成
mkdir coverage
# 1回目: 通常モードで実行
GOCOVERDIR=coverage ./myprogram.exe
# → covcounters.xxx.001.yyy と covmeta.xxx が作成される
# 2回目: フラグ付きで実行(異なる動作)
GOCOVERDIR=coverage ./myprogram.exe -flag
# → covcounters.xxx.002.yyy が追加される(covmetaは既にあるので作られない)
# 3回目: 別のオプションで実行
GOCOVERDIR=coverage ./myprogram.exe -other
# → covcounters.xxx.003.yyy が追加される
# 結果
$ ls coverage/
covcounters.xxx.001.yyy # 1回目のカウンター
covcounters.xxx.002.yyy # 2回目のカウンター
covcounters.xxx.003.yyy # 3回目のカウンター
covmeta.xxx # メタデータ(1つだけ)
2種類のファイルの役割
1. メタデータファイル(covmeta.*)
内容:
- ソースファイル名(
main.go,handlers.goなど) - 関数名(
main(),HandleRequest()など) - コードの構造情報
- 測定対象のコード位置
特徴:
- プログラムの設計図
- 実行ごとに変わらない
- 1回目の実行でのみ作成される
- 2回目以降は作成されない(不要だから)
例え: 試験の「問題用紙」。同じ試験なら何回受けても問題は同じ。
2. カウンターデータファイル(covcounters.*)
内容:
- どのコードが実行されたか
- 各コードが何回実行されたか
- 実行経路の記録
特徴:
- 実行の結果
- 実行ごとに異なる
- 実行するたびに新しいファイルが作成される
例え: 試験の「解答用紙」。試験を受けるたびに新しい解答用紙が必要。
ファイル名の仕組み
1回目の実行:
covmeta.890814fca98ac3a4d41b9bd2a7ec9f7f
covcounters.890814fca98ac3a4d41b9bd2a7ec9f7f.2456041.1670259309405583534
2回目の実行:
covcounters.890814fca98ac3a4d41b9bd2a7ec9f7f.2456047.1670259309410891043
(covmetaは作成されない - 既に存在するから)
ファイル名の構造:
covcounters.【ビルドハッシュ】.【プロセスID】.【タイムスタンプ】
890814fca9... 2456041 1670259309...
covmeta.【ビルドハッシュ】
890814fca9...
- ビルドハッシュ: 同じビルドなら同じ値(890814fca9…)
- プロセスID: 実行ごとに異なる(2456041, 2456047, …)
- タイムスタンプ: 実行日時(ナノ秒単位)
なぜメタデータは1つだけなのか
理由:
同じバイナリから何度実行しても:
- ソースコードは同じ
- 関数名は同じ
- ファイル構造は同じ
つまり、変わらない情報なので、1回保存すれば十分です。
学校の例え:
- メタデータ = クラス名簿(変わらない)
- カウンター = 出席記録(日ごとに異なる)
毎日新しい出席記録は必要だけど、クラス名簿は1つあればOK。
実践例:Webアプリケーションの統合テスト
#!/bin/bash
# 準備
mkdir -p coverage
go build -cover -o webapp ./cmd/server
# テストシナリオ1: ユーザー登録
echo "=== シナリオ1: ユーザー登録 ==="
GOCOVERDIR=coverage ./webapp register --user alice --email alice@example.com
# 結果: covmeta.xxx, covcounters.xxx.001.yyy
# テストシナリオ2: ログイン
echo "=== シナリオ2: ログイン ==="
GOCOVERDIR=coverage ./webapp login --user alice
# 結果: covcounters.xxx.002.yyy(covmetaは作られない)
# テストシナリオ3: プロフィール更新
echo "=== シナリオ3: プロフィール更新 ==="
GOCOVERDIR=coverage ./webapp update-profile --user alice --bio "Hello!"
# 結果: covcounters.xxx.003.yyy
# テストシナリオ4: ログアウト
echo "=== シナリオ4: ログアウト ==="
GOCOVERDIR=coverage ./webapp logout --user alice
# 結果: covcounters.xxx.004.yyy
# 結果確認
echo "=== 生成されたファイル ==="
ls -lh coverage/
# 期待される出力:
# covmeta.abc123 (1つだけ)
# covcounters.abc123.001.* (シナリオ1)
# covcounters.abc123.002.* (シナリオ2)
# covcounters.abc123.003.* (シナリオ3)
# covcounters.abc123.004.* (シナリオ4)
複数回実行のメリット
メリット1: 異なる経路をカバー
// コード例
func ProcessRequest(requestType string) {
if requestType == "GET" {
handleGet() // GETの時だけ実行
} else if requestType == "POST" {
handlePost() // POSTの時だけ実行
} else if requestType == "DELETE" {
handleDelete() // DELETEの時だけ実行
}
}
1回だけ実行(GET):
GOCOVERDIR=coverage ./app --method GET
# → handleGet() だけカバー(33%)
複数回実行:
GOCOVERDIR=coverage ./app --method GET
GOCOVERDIR=coverage ./app --method POST
GOCOVERDIR=coverage ./app --method DELETE
# → すべてカバー(100%)
メリット2: 条件分岐を網羅
func Authenticate(user string, password string) error {
if user == "" {
return errors.New("user required") // ケース1
}
if password == "" {
return errors.New("password required") // ケース2
}
// 正常系
return nil // ケース3
}
包括的テスト:
# エラーケース1
GOCOVERDIR=coverage ./app --user "" --pass "abc"
# エラーケース2
GOCOVERDIR=coverage ./app --user "alice" --pass ""
# 正常ケース
GOCOVERDIR=coverage ./app --user "alice" --pass "secret"
長時間実行プログラムの例
# Webサーバーの統合テスト
go build -cover -o server ./cmd/server
# サーバーをバックグラウンドで起動
GOCOVERDIR=coverage ./server &
SERVER_PID=$!
# さまざまなAPIエンドポイントをテスト
curl http://localhost:8080/api/users # 実行1
curl http://localhost:8080/api/posts # 実行2
curl http://localhost:8080/api/comments # 実行3
curl -X POST http://localhost:8080/api/login # 実行4
# サーバーを停止(この時点でカバレッジデータが保存される)
kill $SERVER_PID
# 結果確認
ls coverage/
# → covcounters.xxx.12345.yyy (1つだけ - 1つのプロセスだから)
# → covmeta.xxx
注意: 長時間実行プログラムの場合、プロセスが1つなのでcovcountersファイルも1つだけ生成されます。
データの蓄積と統合
すべてのcovcountersファイルは、レポート生成時に自動的に統合されます:
# 個別のファイル(生のデータ)
coverage/
├── covmeta.xxx
├── covcounters.xxx.001.yyy # 20%カバー
├── covcounters.xxx.002.yyy # 30%カバー
└── covcounters.xxx.003.yyy # 25%カバー
# ↓ レポート生成時に統合される
# 統合結果: 全体で50%カバー(重複を考慮)
トラブルシューティング
問題1: 2回目の実行でファイルが増えない
$ GOCOVERDIR=coverage ./app
$ GOCOVERDIR=coverage ./app
$ ls coverage/
# covcounters が1つしかない!
原因:
- プロセスIDとタイムスタンプが同じ
- 実行が速すぎて同じタイミングになった
解決策:
# 少し待ってから実行
GOCOVERDIR=coverage ./app
sleep 1
GOCOVERDIR=coverage ./app
問題2: 古いメタデータと新しいカウンターの不一致
# 古いバージョンでビルド
go build -cover -o app-v1 .
GOCOVERDIR=coverage ./app-v1
# コードを変更して再ビルド
# (コードを編集)
go build -cover -o app-v2 .
GOCOVERDIR=coverage ./app-v2 # 同じディレクトリに保存
問題:
- メタデータとカウンターのビルドハッシュが異なる
- レポート生成時にエラーになる可能性
解決策:
# バージョンごとに別ディレクトリを使う
GOCOVERDIR=coverage/v1 ./app-v1
GOCOVERDIR=coverage/v2 ./app-v2
ベストプラクティス
1. シナリオごとにスクリプト化
#!/bin/bash
# test-scenarios.sh
COVERAGE_DIR="coverage"
APP="./myapp"
mkdir -p $COVERAGE_DIR
echo "シナリオ1: 新規ユーザー"
GOCOVERDIR=$COVERAGE_DIR $APP scenario1
echo "シナリオ2: 既存ユーザー"
GOCOVERDIR=$COVERAGE_DIR $APP scenario2
echo "シナリオ3: 管理者"
GOCOVERDIR=$COVERAGE_DIR $APP scenario3
echo "完了!ファイル一覧:"
ls -l $COVERAGE_DIR/
2. クリーンアップを含める
#!/bin/bash
# 古いデータをクリア
rm -rf coverage/*
# ビルド
go build -cover -o app .
# テスト実行
for scenario in scenario1 scenario2 scenario3; do
echo "Running $scenario..."
GOCOVERDIR=coverage ./app $scenario
done
echo "テスト完了"
3. 実行記録を残す
#!/bin/bash
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
COVERAGE_DIR="coverage_$TIMESTAMP"
mkdir -p $COVERAGE_DIR
# テスト実行ログを記録
{
echo "テスト開始: $(date)"
GOCOVERDIR=$COVERAGE_DIR ./app test1
echo "test1 完了"
GOCOVERDIR=$COVERAGE_DIR ./app test2
echo "test2 完了"
echo "テスト終了: $(date)"
} | tee $COVERAGE_DIR/test.log
まとめ
| 項目 | 説明 |
|---|---|
| メタデータ | 1回目の実行でのみ作成される(設計図) |
| カウンター | 実行するたびに作成される(実行記録) |
| 複数回実行 | 同じGOCOVERDIRに蓄積できる |
| 統合 | レポート生成時に自動的に統合される |
| ファイル名 | プロセスIDとタイムスタンプで一意になる |
重要なポイント:
- 同じディレクトリに複数回実行のデータを蓄積できる
- メタデータは1つだけ(変わらない情報)
- カウンターは実行ごとに生成(実行結果)
- すべてのデータは最終的に統合されて1つのレポートになる
次の記事では、これらの生データからレポートを生成する方法を見ていきます。
おわりに
本日は、Go言語の統合テストのカバレッジについて解説しました。

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)

コメント