🔖

TerraformのDrift検知をリアルタイム化するOSSを作った話 

に公開

Go言語プロジェクトのテストカバレッジを0%から52%に向上させた話

TL;DR

  • 🎯 成果: テストカバレッジ 0% → 52.2% (4週間)
  • 📝 テスト数: 200+テストケース、11ファイル
  • 🚀 CI/CD: GitHub Actions + golangci-lint (17 linters)
  • 🛠️ ツール: testify, httptest, カスタムモック

プロジェクト概要

TFDrift-Falco: Falcoを使ったTerraformドリフト検出ツール(2,624行)

Before:
├── テストコード: 0行
├── カバレッジ: 0%
└── CI/CD: なし

After:
├── テストコード: ~3,000行
├── カバレッジ: 52.2%
└── CI/CD: 完全自動化

4フェーズ戦略

Phase 1: 基盤(Week 1)

対象: pkg/types, pkg/config

func TestLoad_ValidConfig(t *testing.T) {
    cfg, err := Load("testdata/valid_config.yaml")
    require.NoError(t, err)
    assert.True(t, cfg.Providers.AWS.Enabled)
}

成果: 90.9%カバレッジ ✅

Phase 2: コアロジック(Week 2)

対象: pkg/terraform/state, pkg/detector

スレッドセーフティのテスト:

func TestStateManager_ThreadSafety(t *testing.T) {
    // 10 goroutines で同時アクセス
    for i := 0; i < 10; i++ {
        go func() {
            resource, exists := sm.GetResource("i-123")
            assert.True(t, exists)
            done <- true
        }()
    }
}

成果: state.goは100%カバレッジ ✅

Phase 3: 統合機能(Week 3)

対象: pkg/diff, pkg/metrics

5種類のdiffフォーマッター:

tests := []string{"Console", "UnifiedDiff", "Markdown", "JSON", "SideBySide"}
for _, format := range tests {
    t.Run(format, func(t *testing.T) {
        // Test each format
    })
}

成果: 96.0%カバレッジ ✅

Phase 4: 外部依存(Week 4)

対象: pkg/falco, pkg/notifier, pkg/terraform/*

MockHTTPServerでWebhookテスト:

func TestSend_Slack(t *testing.T) {
    mockServer := testutil.NewMockHTTPServer()
    defer mockServer.Close()

    manager.Send(alert)

    assert.Equal(t, 1, mockServer.GetRequestCount())
    payload := mockServer.GetLastRequestBody()
    // Verify payload
}

成果: 63-95.5%カバレッジ ✅

CI/CD構築

GitHub Actions

# .github/workflows/test.yml
jobs:
  test:
    strategy:
      matrix:
        go-version: ['1.21', '1.22', '1.23']
    steps:
      - run: go test -race -coverprofile=coverage.out ./...
      - name: Check threshold
        run: |
          COVERAGE=$(go tool cover -func=coverage.out | grep total)
          if (( COVERAGE < 30.0 )); then exit 1; fi

Makefile

ci: deps fmt lint test-coverage-threshold test-race
	@echo "✅ All CI checks passed!"

テストユーティリティ

pkg/testutil パッケージ

// fixtures.go - テストデータ生成
func CreateTestDriftAlert() *types.DriftAlert { ... }
func CreateTestConfig() *config.Config { ... }

// mock_http.go - HTTPモック
type MockHTTPServer struct {
    Server        *httptest.Server
    requests      []*http.Request
    requestBodies []string
}

// mock_falco.go - Falcoクライアントモック
type MockFalcoClient struct {
    events []*types.Event
}

遭遇した課題

1. Prometheus重複登録エラー

問題:

panic: duplicate metrics collector registration attempted

解決: Singleton pattern

var testMetrics *Metrics
func init() { testMetrics = NewMetrics("test") }

func TestRecordDriftAlert(t *testing.T) {
    m := testMetrics // 全テストで同じインスタンス
}

2. JSON unmarshalの型変換

問題: intfloat64 への変換

// Before (失敗):
assert.Equal(t, 0xFF0000, embed["color"])

// After (成功):
assert.Equal(t, float64(0xFF0000), embed["color"])

3. nil vs 空スライス

// Before:
expected: []string{},

// After:
expected: nil,  // Go では nil == empty slice

最終結果

パッケージ カバレッジ 評価
pkg/diff 96.0% ⭐⭐⭐
pkg/notifier 95.5% ⭐⭐⭐
pkg/config 90.9% ⭐⭐⭐
pkg/metrics 81.2% ⭐⭐
pkg/terraform 77.2% ⭐⭐
pkg/falco 63.0%
全体 52.2%

ベストプラクティス

1. Table-Driven Tests

tests := []struct {
    name string
    input string
    want string
}{
    {"case1", "input1", "output1"},
    {"case2", "input2", "output2"},
}

for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        got := function(tt.input)
        assert.Equal(t, tt.want, got)
    })
}

2. t.Helper() の活用

func setupTest(t *testing.T) (*Config, func()) {
    t.Helper()  // スタックトレースから除外
    // setup logic
    return config, cleanup
}

3. 段階的アプローチ

Week 1: 基盤(簡単)     → 15%
Week 2: コア(中程度)   → 31%
Week 3: 統合(やや難)   → 37%
Week 4: 外部依存(難)   → 52%

学んだこと

✅ Do's

  • 依存関係の少ないパッケージから始める
  • テストユーティリティを早めに整備
  • CI/CDを同時に構築
  • モックは必要最小限に

❌ Don'ts

  • 全てを一度にテストしようとしない
  • 複雑なモックを作りすぎない
  • カバレッジだけを追わない
  • テストのメンテナンスを怠らない

次のステップ

短期(1-2ヶ月)

  • pkg/detector: 21% → 60%
  • cmd/: 0% → 30%
  • 統合テスト追加

中期(3-6ヶ月)

  • カバレッジ80%達成
  • パフォーマンステスト
  • Fuzzing導入

長期(6ヶ月+)

  • Mutation testing
  • カオスエンジニアリング

まとめ

テストカバレッジ向上は単なる数値目標ではなく、開発文化の変革です:

  • 🎯 コードの信頼性向上
  • 🚀 安心してリファクタリング
  • 🐛 バグの早期発見
  • 📚 実行可能なドキュメント
  • 🤝 チーム開発の基盤

重要なのは、完璧を目指さず、段階的に改善し続けることです。

コード例

完全なコード例は以下を参照:


執筆: 2025年11月18日
Tags: #golang #testing #cicd #devops

Discussion