mcpField
G
golang-testing-strategies

golang-testing-strategies

github.com/bobmatnycテスト・品質78449 回インストール15 週間前に更新
リポジトリ

golang-testing-strategies について

Go 開発者向けのテスト戦略ガイドです。標準の `testing` パッケージや Testify、Gomock を使った単体・統合テスト、ベンチマーク、並行処理の検査、カバレッジを扱います。

主な機能

  1. 01テーブル駆動テストとサブテスト
  2. 02Testify のアサーションとスイート
  3. 03Gomock によるインターフェースのモック
  4. 04ベンチマークとメモリ分析
  5. 05競合検出とカバレッジ
  6. 06CI テスト連携

活用シーン

  1. 01Go ライブラリやアプリのテスト作成
  2. 02データベースや API などの依存先のモック
  3. 03並行コードのデータ競合確認
  4. 04CI へのテストとカバレッジ導入

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill golang-testing-strategies

インストール後、Claude Code で/golang-testing-strategiesを入力して読み込まれたか確認します。

同梱ファイルmetadata.json

詳細

Go のテスト戦略

概要

Go には、シンプルさと開発者の生産性を重視した、堅牢な組み込みテストフレームワーク(testing パッケージ)が用意されています。testify や gomock などのコミュニティ製ツールと組み合わせることで、ボイラープレートを最小限に抑えながら、包括的なテストカバレッジを実現できます。

主な機能:

  • 📋 テーブル駆動テスト:複数の入力をテストするための慣用的なパターンです
  • ✅ Testify:読みやすいアサーションとテストスイートです
  • 🎭 Gomock:型安全なインターフェイスモックです
  • ⚡ ベンチマーク:組み込みのパフォーマンステストです
  • 🔍 競合検出器:並行コードの安全性を検証します
  • 📊 カバレッジ:ネイティブのカバレッジレポートと適用機能です
  • 🚀 CI 連携:テストのキャッシュと並列実行に対応します

このスキルを使用するタイミング

次の場合にこのスキルを有効にします:

  • Go ライブラリやアプリケーションのテストスイートを作成するとき
  • 新しいプロジェクトのテスト基盤を設定するとき
  • 外部依存関係(データベース、API、サービス)をモックするとき
  • パフォーマンスが重要なコードパスのベンチマークを測定するとき
  • スレッドセーフな並行実装を確認するとき
  • テストを CI/CD パイプラインに統合するとき
  • 他のテストフレームワークから移行するとき

テストの基本原則

Go のテスト哲学
  1. マジックよりシンプルさ:可能な限り標準ライブラリを使用します
  2. テーブル駆動テスト:1 つの関数で複数のシナリオをテストします
  3. サブテスト:t.Run() で関連するテストをまとめます
  4. インターフェイスベースのモック:インターフェイスを介して依存関係をモックします
  5. テストファイルをコードと同じ場所に配置:*_test.go ファイルをコードと同じディレクトリに置きます
  6. パッケージの命名:外部テストには package_test、内部テストには package を使用します
テストの構成

ファイル名の規則:

  • 単体テスト:file_test.go
  • 統合テスト:file_integration_test.go
  • ベンチマークテスト:同じテストファイル内で Benchmark を接頭辞として付けます

パッケージ構成:

mypackage/
├── user.go
├── user_test.go              // Internal tests (same package)
├── user_external_test.go     // External tests (package mypackage_test)
├── integration_test.go       // Integration tests
└── testdata/                 // Test fixtures (ignored by go build)
    └── golden.json

テーブル駆動テストのパターン

基本構造

複数の入力をテストするための、Go で慣用的なテストパターンです:

func TestUserValidation(t *testing.T) {
    tests := []struct {
        name    string
        input   User
        wantErr bool
        errMsg  string
    }{
        {
            name:    "valid user",
            input:   User{Name: "Alice", Age: 30, Email: "[email protected]"},
            wantErr: false,
        },
        {
            name:    "empty name",
            input:   User{Name: "", Age: 30, Email: "[email protected]"},
            wantErr: true,
            errMsg:  "name is required",
        },
        {
            name:    "invalid email",
            input:   User{Name: "Bob", Age: 25, Email: "invalid"},
            wantErr: true,
            errMsg:  "invalid email format",
        },
        {
            name:    "negative age",
            input:   User{Name: "Charlie", Age: -5, Email: "[email protected]"},
            wantErr: true,
            errMsg:  "age must be positive",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateUser(tt.input)

            if (err != nil) != tt.wantErr {
                t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
                return
            }

            if tt.wantErr && err.Error() != tt.errMsg {
                t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
            }
        })
    }
}
テストの並列実行

独立したテストで並列実行を有効にします:

func TestConcurrentOperations(t *testing.T) {
    tests := []struct {
        name string
        fn   func() int
        want int
    }{
        {"operation 1", func() int { return compute1() }, 42},
        {"operation 2", func() int { return compute2() }, 84},
        {"operation 3", func() int { return compute3() }, 126},
    }

    for _, tt := range tests {
        tt := tt // Capture range variable
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // Run tests concurrently

            got := tt.fn()
            if got != tt.want {
                t.Errorf("got %v, want %v", got, tt.want)
            }
        })
    }
}

Testify フレームワーク

インストール
go get github.com/stretchr/testify
アサーション

冗長なエラーチェックを、読みやすいアサーションに置き換えます:

import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestCalculator(t *testing.T) {
    calc := NewCalculator()

    // assert: Test continues on failure
    assert.Equal(t, 5, calc.Add(2, 3))
    assert.NotNil(t, calc)
    assert.True(t, calc.IsReady())

    // require: Test stops on failure (for critical assertions)
    result, err := calc.Divide(10, 2)
    require.NoError(t, err) // Stop if error occurs
    assert.Equal(t, 5, result)
}

func TestUserOperations(t *testing.T) {
    user := &User{ID: 1, Name: "Alice", Email: "[email protected]"}

    // Object matching
    assert.Equal(t, 1, user.ID)
    assert.Contains(t, user.Email, "@")
    assert.Len(t, user.Name, 5)

    // Partial matching
    assert.ObjectsAreEqual(user, &User{
        ID:    1,
        Name:  "Alice",
        Email: assert.AnythingOfType("string"),
    })
}
テストスイート

セットアップ/後処理を使って関連するテストをまとめます:

import (
    "testing"
    "github.com/stretchr/testify/suite"
)

type UserServiceTestSuite struct {
    suite.Suite
    db      *sql.DB
    service *UserService
}

// SetupSuite runs once before all tests
func (s *UserServiceTestSuite) SetupSuite() {
    s.db = setupTestDatabase()
    s.service = NewUserService(s.db)
}

// TearDownSuite runs once after all tests
func (s *UserServiceTestSuite) TearDownSuite() {
    s.db.Close()
}

// SetupTest runs before each test
func (s *UserServiceTestSuite) SetupTest() {
    cleanDatabase(s.db)
}

// TearDownTest runs after each test
func (s *UserServiceTestSuite) TearDownTest() {
    // Cleanup if needed
}

// Test methods must start with "Test"
func (s *UserServiceTestSuite) TestCreateUser() {
    user := &User{Name: "Alice", Email: "[email protected]"}

    err := s.service.Create(user)
    s.NoError(err)
    s.NotEqual(0, user.ID) // ID assigned
}

func (s *UserServiceTestSuite) TestGetUser() {
    // Setup
    user := &User{Name: "Bob", Email: "[email protected]"}
    s.service.Create(user)

    // Test
    retrieved, err := s.service.GetByID(user.ID)
    s.NoError(err)
    s.Equal(user.Name, retrieved.Name)
}

// Run the suite
func TestUserServiceTestSuite(t *testing.T) {
    suite.Run(t, new(UserServiceTestSuite))
}

Gomock によるインターフェイスのモック

インストール
go install github.com/golang/mock/mockgen@latest
モックの生成
// user_repository.go
package repository

//go:generate mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks

type UserRepository interface {
    GetByID(id int) (*User, error)
    Create(user *User) error
    Update(user *User) error
    Delete(id int) error
}

モックを生成します:

go generate ./...
# Or manually:
mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks
テストでのモックの使用
import (
    "testing"
    "github.com/golang/mock/gomock"
    "github.com/stretchr/testify/assert"
    "myapp/repository/mocks"
)

func TestUserService_GetUser(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    // Create mock
    mockRepo := mocks.NewMockUserRepository(ctrl)

    // Set expectations
    expectedUser := &User{ID: 1, Name: "Alice"}
    mockRepo.EXPECT().
        GetByID(1).
        Return(expectedUser, nil).
        Times(1)

    // Test
    service := NewUserService(mockRepo)
    user, err := service.GetUser(1)

    // Assertions
    assert.NoError(t, err)
    assert.Equal(t, expectedUser, user)
}

func TestUserService_CreateUser_Validation(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := mocks.NewMockUserRepository(ctrl)

    // Expect Create to NOT be called (validation should fail first)
    mockRepo.EXPECT().Create(gomock.Any()).Times(0)

    service := NewUserService(mockRepo)
    err := service.CreateUser(&User{Name: ""}) // Invalid user

    assert.Error(t, err)
    assert.Contains(t, err.Error(), "name is required")
}
カスタムマッチャー
// Custom matcher for complex validation
type userMatcher struct {
    expectedEmail string
}

func (m userMatcher) Matches(x interface{}) bool {
    user, ok := x.(*User)
    if !ok {
        return false
    }
    return user.Email == m.expectedEmail
}

func (m userMatcher) String() string {
    return "matches user with email: " + m.expectedEmail
}

func UserWithEmail(email string) gomock.Matcher {
    return userMatcher{expectedEmail: email}
}

// Usage in test
func TestCustomMatcher(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := mocks.NewMockUserRepository(ctrl)

    mockRepo.EXPECT().
        Create(UserWithEmail("[email protected]")).
        Return(nil)

    service := NewUserService(mockRepo)
    service.CreateUser(&User{Name: "Alice", Email: "[email protected]"})
}

ベンチマークテスト

基本的なベンチマーク
func BenchmarkAdd(b *testing.B) {
    calc := NewCalculator()

    for i := 0; i < b.N; i++ {
        calc.Add(2, 3)
    }
}

func BenchmarkStringConcatenation(b *testing.B) {
    b.Run("plus operator", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = "hello" + "world"
        }
    })

    b.Run("strings.Builder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var sb strings.Builder
            sb.WriteString("hello")
            sb.WriteString("world")
            _ = sb.String()
        }
    })
}
ベンチマークの実行
# Run all benchmarks
go test -bench=.

# Run specific benchmark
go test -bench=BenchmarkAdd

# With memory allocation stats
go test -bench=. -benchmem

# Compare benchmarks
go test -bench=. -benchmem > old.txt
# Make changes
go test -bench=. -benchmem > new.txt
benchstat old.txt new.txt
ベンチマーク出力の例
BenchmarkAdd-8                  1000000000      0.25 ns/op      0 B/op      0 allocs/op
BenchmarkStringBuilder-8        50000000        28.5 ns/op      64 B/op     1 allocs/op

読み方:50000000 回の反復、1 操作あたり 28.5 ns/op、1 操作あたり 64 B/op のバイト数を割り当て、1 操作あたり 1 allocs/op 回の割り当て

高度なテストパターン

HTTP ハンドラー向けの httptest
import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestUserHandler(t *testing.T) {
    handler := http.HandlerFunc(UserHandler)

    req := httptest.NewRequest("GET", "/users/1", nil)
    rec := httptest.NewRecorder()

    handler.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.Contains(t, rec.Body.String(), "Alice")
}

func TestHTTPClient(t *testing.T) {
    // Mock server
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        assert.Equal(t, "/api/users", r.URL.Path)
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"id": 1, "name": "Alice"}`))
    }))
    defer server.Close()

    // Test client against mock server
    client := NewAPIClient(server.URL)
    user, err := client.GetUser(1)

    assert.NoError(t, err)
    assert.Equal(t, "Alice", user.Name)
}
競合検出器

並行コード内のデータ競合を検出します:

go test -race ./...

並行安全性を確認するテストの例:

func TestConcurrentMapAccess(t *testing.T) {
    cache := NewSafeCache()

    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(val int) {
            defer wg.Done()
            cache.Set(fmt.Sprintf("key%d", val), val)
        }(i)
    }

    wg.Wait()
    assert.Equal(t, 100, cache.Len())
}
ゴールデンファイルテスト

期待される出力ファイルと照合してテストします:

func TestRenderTemplate(t *testing.T) {
    output := RenderTemplate("user", User{Name: "Alice"})

    goldenFile := "testdata/user_template.golden"

    if *update {
        // Update golden file: go test -update
        os.WriteFile(goldenFile, []byte(output), 0644)
    }

    expected, err := os.ReadFile(goldenFile)
    require.NoError(t, err)

    assert.Equal(t, string(expected), output)
}

var update = flag.Bool("update", false, "update golden files")

CI/CD 連携

GitHub Actions の例
# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.23'

      - name: Run tests
        run: go test -v -race -coverprofile=coverage.out ./...

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage.out

      - name: Check coverage threshold
        run: |
          go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//' | \
          awk '{if ($1 < 80) exit 1}'
カバレッジの適用
# Generate coverage report
go test -coverprofile=coverage.out ./...

# View coverage in terminal
go tool cover -func=coverage.out

# Generate HTML report
go tool cover -html=coverage.out -o coverage.html

# Check coverage threshold (fail if < 80%)
go test -coverprofile=coverage.out ./... && \
go tool cover -func=coverage.out | grep total | awk '{if (substr($3, 1, length($3)-1) < 80) exit 1}'

意思決定ツリー

各テストツールの使いどころ

標準の testing パッケージを使う場合:

  • アサーションが少ないシンプルなユニットテスト
  • モック化する外部依存がない
  • パフォーマンスのベンチマーク
  • 依存関係を最小限に抑えたい

Testify を使う場合:

  • 読みやすいアサーションが必要(冗長なチェックより assert.Equal)
  • セットアップ/ティアダウンを伴うテストスイート
  • 似たテストケースが複数ある
  • 表現力の高いテストコードを使いたい

Gomock を使う場合:

  • インターフェースに依存するコードのテスト
  • 呼び出しの回数や順序を正確に検証したい
  • 複数のシナリオを含む複雑なモック動作
  • 型安全なモックが必要

ベンチマークを使う場合:

  • パフォーマンスが重要なコードを最適化する
  • アルゴリズムの実装を比較する
  • パフォーマンスのリグレッションを検出する
  • メモリ割り当てをプロファイリングする

httptest を使う場合:

  • HTTP ハンドラーをテストする
  • 外部 HTTP API をモック化する
  • HTTP クライアントを統合テストする
  • ミドルウェアチェーンをテストする

レースディテクターを使う場合:

  • 並行コードを書く
  • ゴルーチンやチャネルを使う
  • ゴルーチン間で状態を共有する
  • 並行コードすべてに対して CI/CD で実行する

避けるべきアンチパターン

❌ すべてをモック化しない

// WRONG: Over-mocking makes tests brittle
mockLogger := mocks.NewMockLogger(ctrl)
mockConfig := mocks.NewMockConfig(ctrl)
mockMetrics := mocks.NewMockMetrics(ctrl)
// Too many mocks = fragile test

✅ 外部依存のみをモック化する

// CORRECT: Mock only database, use real logger/config
mockRepo := mocks.NewMockUserRepository(ctrl)
service := NewUserService(mockRepo, realLogger, realConfig)

❌ 実装の詳細をテストしない

// WRONG: Testing internal state
assert.Equal(t, "processing", service.internalState)

✅ 公開されている振る舞いをテストする

// CORRECT: Test observable outcomes
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)

❌ エラーケースを無視しない

// WRONG: Only testing happy path
func TestGetUser(t *testing.T) {
    user, _ := service.GetUser(1) // Ignoring error!
    assert.NotNil(t, user)
}

✅ エラー条件をテストする

// CORRECT: Test both success and error cases
func TestGetUser_NotFound(t *testing.T) {
    user, err := service.GetUser(999)
    assert.Error(t, err)
    assert.Nil(t, user)
    assert.Contains(t, err.Error(), "not found")
}

ベストプラクティス

  1. テストをソースコードと同じ場所に配置する:*_test.go ファイルをソースコードと同じディレクトリに置きます
  2. サブテストを使う:t.Run() で関連するテストを整理します
  3. 安全な場合は並列実行する:独立したテストでは t.Parallel() を有効にします
  4. インターフェースをモック化する:インターフェースへの依存を活用し、テストしやすい設計にします
  5. エラーをテストする:成功パスと失敗パスの両方を検証します
  6. 重要な処理をベンチマークする:パフォーマンスに影響するコードをプロファイリングします
  7. レースディテクターを実行する:並行コードでは必ず -race を使います
  8. カバレッジを確保する:CI で最低しきい値を設定します(通常は 80%)
  9. ゴールデンファイルを使う:複雑な出力を期待されるファイルと比較してテストします
  10. テストを高速に保つ:時間のかかる処理をモック化し、素早く実行する場合は -short フラグを使います

リソース

公式ドキュメント:

テストフレームワーク:

最近のガイド(2025 年):

  • 「Go Unit Testing: Structure & Best Practices」(2025 年 11 月)
  • Go Wiki:テストにおけるよくある間違い
  • Google Go Style Guide - Testing:https://google.github.io/styleguide/go/

関連スキル:

  • golang-engineer:Go の基本パターンと並行処理
  • verification-before-completion:「完了」の一部としてのテスト
  • testing-anti-patterns:よくあるテストの間違いを避ける

クイックリファレンス

テストを実行する
go test ./...                    # All tests
go test -v ./...                 # Verbose output
go test -short ./...             # Skip slow tests
go test -run TestUserCreate      # Specific test
go test -race ./...              # With race detector
go test -cover ./...             # With coverage
go test -coverprofile=c.out ./... # Coverage file
go test -bench=. -benchmem       # Benchmarks with memory
モックを生成する
go generate ./...                           # All //go:generate directives
mockgen -source=interface.go -destination=mock.go
カバレッジを分析する
go tool cover -func=coverage.out           # Coverage per function
go tool cover -html=coverage.out           # HTML report

トークンの推定数:約 4,500 トークン(エントリーポイント+全コンテンツ) バージョン:1.0.0 最終更新日:2025-12-03

コメント