
こんにちは。よっしーです(^^)
本日は、Go言語の.統合テストのカバレッジついて解説しています。
背景
Go言語でテストを書いていると、「go test -coverprofileでカバレッジが取れるのは知っているけど、統合テストのカバレッジってどうやって測るんだろう?」と疑問に思ったことはありませんか?
公式ドキュメントには、Go 1.20から統合テストのカバレッジ測定がサポートされたことが書かれていますが、英語で書かれている上に、ユニットテストとの違いや具体的な手順の説明が簡潔すぎて、初めて読むと「結局どうすればいいの?」と戸惑ってしまうかもしれません。
この記事では、公式ドキュメントの内容を丁寧な日本語に翻訳し、さらに初心者の方でも理解できるように、ユニットテストと統合テストの違い、なぜ3ステップ必要なのか、そして実際にどのようなコマンドを実行すればよいのかを、具体例を交えて解説していきます。
統合テストのカバレッジ測定は一見難しそうに見えますが、仕組みを理解すれば決して複雑ではありません。実際のアプリケーションでどれだけコードがテストされているかを把握することで、より品質の高いソフトウェア開発ができるようになります。一緒に学んでいきましょう!
プログラムがパニックした場合、カバレッジデータは書き込まれますか?
go build -coverでビルドされたプログラムは、プログラムがos.Exit()を呼び出すか、main.mainから正常に返る場合にのみ、実行終了時に完全なプロファイルデータを書き出します。プログラムが回復されないパニックで終了した場合、またはプログラムが致命的な例外(セグメンテーション違反、ゼロ除算など)に遭遇した場合、実行中に実行されたステートメントのプロファイルデータは失われます。
解説
カバレッジデータが保存されるタイミング
重要な原則
カバレッジデータはプログラムが正常に終了した時にのみ保存されます。
レストランの例え:
- 営業終了時に売上記録を保存する(正常終了)
- 突然停電になると記録が保存されない(異常終了)
- 記録用紙に書いていた途中のデータも失われる(パニック)
カバレッジデータが保存される場合
ケース1: 正常終了(return)
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
// main関数から正常に返る
}
結果:
$ go build -cover -o myapp .
$ GOCOVERDIR=coverage ./myapp
Hello, World!
$ ls coverage/
covcounters.xxx covmeta.xxx # ✅ データが保存される
ケース2: os.Exit(0) で終了
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("Processing...")
// 正常終了コードで明示的に終了
os.Exit(0) // ✅ カバレッジデータが保存される
}
結果:
$ GOCOVERDIR=coverage ./myapp
Processing...
$ ls coverage/
covcounters.xxx covmeta.xxx # ✅ データが保存される
ケース3: エラーコード付き終了(os.Exit(1))
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("Error occurred!")
// エラーコードで終了
os.Exit(1) // ✅ カバレッジデータは保存される(終了コードに関わらず)
}
結果:
$ GOCOVERDIR=coverage ./myapp
Error occurred!
$ echo $?
1 # 終了コードは1(エラー)
$ ls coverage/
covcounters.xxx covmeta.xxx # ✅ データは保存される
カバレッジデータが失われる場合
ケース1: 回復されないパニック
package main
import "fmt"
func main() {
fmt.Println("Starting...")
// パニックが発生
panic("something went wrong!") // ❌ カバレッジデータが失われる
fmt.Println("This will never run")
}
結果:
$ GOCOVERDIR=coverage ./myapp
Starting...
panic: something went wrong!
goroutine 1 [running]:
main.main()
/path/to/main.go:7 +0x...
exit status 2
$ ls coverage/
# ❌ ファイルが作成されない、またはデータが不完全
ケース2: セグメンテーション違反(segmentation fault)
package main
import (
"fmt"
"unsafe"
)
func main() {
fmt.Println("Starting...")
// 危険な操作でセグメンテーション違反
var ptr *int
*ptr = 42 // ❌ クラッシュ!カバレッジデータが失われる
fmt.Println("This will never run")
}
結果:
$ GOCOVERDIR=coverage ./myapp
Starting...
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 …]
$ ls coverage/ # ❌ データが保存されない
ケース3: ゼロ除算
package main
import "fmt"
func main() {
fmt.Println("Calculating...")
x := 10
y := 0
z := x / y // ❌ パニック!カバレッジデータが失われる
fmt.Println(z)
}
結果:
$ GOCOVERDIR=coverage ./myapp
Calculating...
panic: runtime error: integer divide by zero
$ ls coverage/
# ❌ データが保存されない
パニックから回復してデータを保存
解決策: deferとrecoverを使用
package main
import (
"fmt"
"os"
)
func main() {
// パニックを回復するためのdefer
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "Recovered from panic: %v\n", r)
// 正常終了としてプログラムを終了
os.Exit(1) // ✅ カバレッジデータが保存される
}
}()
fmt.Println("Starting...")
// ここでパニックが発生しても...
panic("something went wrong!")
fmt.Println("This will never run")
}
結果:
$ GOCOVERDIR=coverage ./myapp
Starting...
Recovered from panic: something went wrong!
$ echo $?
1 # エラーコードで終了
$ ls coverage/
covcounters.xxx covmeta.xxx # ✅ データが保存される!
実践例1: エラーハンドリング付きWebサーバー
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
)
func main() {
// グローバルなパニック回復
defer func() {
if r := recover(); r != nil {
log.Printf("Fatal error recovered: %v", r)
// カバレッジデータを保存して終了
os.Exit(1)
}
}()
// シグナルハンドリング(Ctrl+Cなど)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
fmt.Println("\nShutting down gracefully...")
os.Exit(0) // ✅ 正常終了でカバレッジ保存
}()
// Webサーバー起動
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
fmt.Println("Server starting on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err) // ✅ log.Fatalはos.Exit(1)を呼ぶので保存される
}
}
使用例:
$ go build -cover -o webserver .
$ GOCOVERDIR=coverage ./webserver &
Server starting on :8080
# テストリクエスト
$ curl http://localhost:8080/
Hello, World!
# 正常終了(Ctrl+C)
$ kill -SIGINT [PID]
Shutting down gracefully...
$ ls coverage/
covcounters.xxx covmeta.xxx # ✅ データが保存される
実践例2: テストランナーでの適切なエラーハンドリング
package main
import (
"fmt"
"os"
)
func runTests() error {
// テスト1
if err := test1(); err != nil {
return fmt.Errorf("test1 failed: %w", err)
}
// テスト2
if err := test2(); err != nil {
return fmt.Errorf("test2 failed: %w", err)
}
// テスト3
if err := test3(); err != nil {
return fmt.Errorf("test3 failed: %w", err)
}
return nil
}
func main() {
// パニック回復
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "PANIC: %v\n", r)
os.Exit(2) // ✅ カバレッジ保存
}
}()
// テスト実行
if err := runTests(); err != nil {
fmt.Fprintf(os.Stderr, "Tests failed: %v\n", err)
os.Exit(1) // ✅ カバレッジ保存
}
fmt.Println("All tests passed!")
os.Exit(0) // ✅ カバレッジ保存
}
func test1() error { return nil }
func test2() error { return nil }
func test3() error { return nil }
終了方法の比較
| 終了方法 | カバレッジ保存 | 説明 |
|---|---|---|
return from main | ✅ 保存される | 最も推奨される方法 |
os.Exit(0) | ✅ 保存される | 正常終了 |
os.Exit(1) | ✅ 保存される | エラー終了(でもデータは保存) |
panic() (未回復) | ❌ 失われる | データが保存されない |
panic() + recover() + os.Exit() | ✅ 保存される | 回復して正常終了すればOK |
log.Fatal() | ✅ 保存される | 内部でos.Exit(1)を呼ぶ |
log.Panic() | ❌ 失われる | panic()を呼ぶので失われる |
| Segmentation fault | ❌ 失われる | プロセスクラッシュ |
Kill signal (SIGKILL) | ❌ 失われる | 即座に終了 |
Interrupt (SIGINT) + handler | ✅ 保存される | ハンドラでos.Exit()すればOK |
実践例3: 包括的なエラーハンドリング
package main
import (
"fmt"
"log"
"os"
"os/signal"
"runtime/debug"
"syscall"
)
func main() {
// 終了コード
exitCode := 0
// パニック回復 - 最後の砦
defer func() {
if r := recover(); r != nil {
log.Printf("PANIC RECOVERED: %v\n", r)
log.Printf("Stack trace:\n%s", debug.Stack())
exitCode = 2
}
// 常にos.Exit()で終了してカバレッジを保存
os.Exit(exitCode)
}()
// シグナルハンドリング
setupSignalHandlers()
// メインロジック
if err := run(); err != nil {
log.Printf("Error: %v", err)
exitCode = 1
return // deferが実行される
}
log.Println("Success!")
exitCode = 0
}
func setupSignalHandlers() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
sig := <-sigChan
log.Printf("Received signal: %v", sig)
log.Println("Shutting down gracefully...")
os.Exit(0) // シグナル受信時も正常終了
}()
}
func run() error {
// 実際のビジネスロジック
fmt.Println("Running application...")
// 何らかの処理
// ...
return nil
}
テストスクリプトの例
#!/bin/bash
# test-coverage-on-panic.sh
APP="./myapp"
COVERAGE_DIR="coverage"
echo "=== カバレッジデータ保存テスト ==="
# クリーンアップ
rm -rf $COVERAGE_DIR
# ビルド
go build -cover -o $APP .
# テスト1: 正常終了
echo ""
echo "【テスト1: 正常終了】"
rm -rf $COVERAGE_DIR && mkdir $COVERAGE_DIR
GOCOVERDIR=$COVERAGE_DIR $APP --mode=normal
if [ -f "$COVERAGE_DIR/covcounters."* ]; then
echo "✅ カバレッジデータが保存されました"
else
echo "❌ カバレッジデータが保存されませんでした"
fi
# テスト2: エラー終了(os.Exit(1))
echo ""
echo "【テスト2: エラー終了】"
rm -rf $COVERAGE_DIR && mkdir $COVERAGE_DIR
GOCOVERDIR=$COVERAGE_DIR $APP --mode=error
if [ -f "$COVERAGE_DIR/covcounters."* ]; then
echo "✅ カバレッジデータが保存されました"
else
echo "❌ カバレッジデータが保存されませんでした"
fi
# テスト3: パニック(回復あり)
echo ""
echo "【テスト3: パニック(回復あり)】"
rm -rf $COVERAGE_DIR && mkdir $COVERAGE_DIR
GOCOVERDIR=$COVERAGE_DIR $APP --mode=panic-recovered
if [ -f "$COVERAGE_DIR/covcounters."* ]; then
echo "✅ カバレッジデータが保存されました"
else
echo "❌ カバレッジデータが保存されませんでした"
fi
# テスト4: パニック(回復なし)
echo ""
echo "【テスト4: パニック(回復なし)】"
rm -rf $COVERAGE_DIR && mkdir $COVERAGE_DIR
GOCOVERDIR=$COVERAGE_DIR $APP --mode=panic-unrecovered 2>/dev/null
if [ -f "$COVERAGE_DIR/covcounters."* ]; then
echo "❌ 予期しないデータ保存"
else
echo "✅ 予想通りデータは保存されませんでした"
fi
echo ""
echo "テスト完了"
ベストプラクティス
1. 常にdefer + recoverを使用
func main() {
defer func() {
if r := recover(); r != nil {
log.Printf("Fatal error: %v", r)
os.Exit(1) // カバレッジ保存
}
}()
// メインロジック
}
2. シグナルハンドリングを実装
func main() {
// シグナルをキャッチして正常終了
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
cleanup()
os.Exit(0)
}()
// メインロジック
}
3. エラーは適切に処理
func main() {
if err := run(); err != nil {
log.Printf("Error: %v", err)
os.Exit(1) // panic()ではなくos.Exit()
}
}
まとめ
カバレッジデータが保存される条件:
- ✅
mainから正常にreturn - ✅
os.Exit()を呼び出す(終了コードは問わない) - ✅ パニックを
recoverしてos.Exit()
カバレッジデータが失われる条件:
- ❌ 回復されない
panic() - ❌ セグメンテーション違反などの致命的エラー
- ❌
SIGKILLなどの即座終了シグナル
重要なポイント:
- カバレッジデータはプログラム終了時に書き込まれる
- パニックや異常終了ではデータが失われる
deferとrecoverで回復すればデータを保存できる- 統合テストでは適切なエラーハンドリングが必須
- シグナルハンドリングで正常終了を保証する
推奨パターン:
func main() {
// パニック回復
defer func() {
if r := recover(); r != nil {
log.Printf("Panic: %v", r)
os.Exit(1)
}
}()
// シグナルハンドリング
setupGracefulShutdown()
// エラー処理
if err := run(); err != nil {
log.Printf("Error: %v", err)
os.Exit(1)
}
}
おわりに
本日は、Go言語の統合テストのカバレッジについて解説しました。

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

コメント