こんにちは!フリーランスエンジニアのこたろうです。
今回は、Goのテストカバレッジについて、実践的な知見を共有します。
なぜカバレッジを計測するのか?
テストカバレッジとは、コードがテストによってどの程度カバーされているかを示す指標です。
これを計測する理由は:
- バグの早期発見
- コードの品質維持
- テスト漏れの防止
基本的な使い方
1. カバレッジの計測
# 基本的な実行方法
go test -cover
# 詳細な結果を出力
go test -cover -v
# カバレッジファイルの生成
go test -coverprofile=coverage.out
2. カバレッジレポートの可視化
# HTMLレポートの生成
go tool cover -html=coverage.out -o coverage.html
# ターミナルでの表示
go tool cover -func=coverage.out
実践的な例
テスト対象のコード
// calc/calc.go
package calc
func Add(a, b int) int {
return a + b
}
func Subtract(a, b int) int {
return a - b
}
func Multiply(a, b int) int {
return a * b
}
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
テストコード
// calc/calc_test.go
package calc
import (
"testing"
)
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
}
})
}
}
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
expectError bool
}{
{"normal division", 6, 2, 3, false},
{"division by zero", 6, 0, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Divide(tt.a, tt.b)
if tt.expectError && err == nil {
t.Error("expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("unexpected error: %v", err)
}
if !tt.expectError && result != tt.expected {
t.Errorf("Divide(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
}
})
}
}
カバレッジレポートの読み方
func単位のレポート
$ go tool cover -func=coverage.out
calc/calc.go:3: Add 100.0%
calc/calc.go:7: Subtract 0.0%
calc/calc.go:11: Multiply 0.0%
calc/calc.go:15: Divide 100.0%
total: (statements) 66.7%
この結果から分かること:
- Add関数は100%カバー
- Subtract関数は未テスト
- Multiply関数は未テスト
- Divide関数は100%カバー
- 全体のカバレッジは66.7%
カバレッジ向上のコツ
- テーブル駆動テストの活用
func TestMultiply(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 6},
{"negative", -2, 3, -6},
{"zero", 0, 5, 0},
}
// ...
}
- エッジケースのテスト
func TestDivide_EdgeCases(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
expectError bool
}{
{"max int", math.MaxInt64, 1, math.MaxInt64, false},
{"min int", math.MinInt64, 1, math.MinInt64, false},
{"division by zero", 1, 0, 0, true},
}
// ...
}
- エラーケースのテスト
func TestDivide_Error(t *testing.T) {
_, err := Divide(1, 0)
if err == nil {
t.Error("expected error for division by zero")
}
}
CI/CDでの活用
GitHub Actionsでの例
name: Go Test Coverage
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
- name: Run Tests with Coverage
run: |
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
- name: Upload Coverage Report
uses: actions/upload-artifact@v2
with:
name: coverage-report
path: coverage.out
まとめ
テストカバレッジを計測・可視化することで:
- コードの品質を定量的に評価可能
- テスト漏れを早期発見
- リファクタリング時の安全性確保
ただし、カバレッジ100%を目指すことが必ずしも最適とは限りません。
コストとベネフィットを考慮しながら、適切なカバレッジ目標を設定することが重要です。