Communitygithub.com

Go テストパターン

テーブル駆動テスト、サブテスト、ベンチマーク、ファジング、テストカバレッジを含むGoのテストパターンです。TDD手法に従い慣用的なGoのプラクティスを使用します。Goのテストを書く際に使用します — テーブル駆動テスト、サブテスト、ベンチマーク、ファジング、カバレッジ。

Go テストパターン とは?

このスキルはGo言語における包括的なテストパターンを提供し、開発者がTDD(テスト駆動開発)のワークフローに従って信頼性が高く保守しやすいテストを書くことを可能にします。RED-GREEN-REFACTORサイクルに基づき、最初に失敗するテストを書き、最小限のコードでテストを通し、その後リファクタリングを行うという反復的なプロセスを支援します。テーブル駆動テストにより複数のテストケースを効率的にカバーでき、サブテストやサブベンチマークでテストを整理し、t.Parallel()による並列実行もサポートします。さらに、テストヘルパー、ゴールデンファイル、インターフェースベースのモッキング、HTTPハンドラテスト、カバレッジ計測、ファジング(Go 1.18+)など、実践的なテスト技法を網羅しています。このスキルはClaude Code, Cursor, CodexなどのAIエージェントで利用でき、テストコードの生成や改善を自動化します。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/golang-testing

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Go テストパターン は何をしますか?

TDD手法に従い、信頼性が高く保守しやすいテストを書くための包括的なGoテストパターン。

アクティブにするタイミング

  • 新しいGoの関数やメソッドを書くとき
  • 既存のコードにテストカバレッジを追加するとき
  • パフォーマンスが重要なコードのベンチマークを作成するとき
  • 入力バリデーションのファズテストを実装するとき
  • GoプロジェクトでTDDワークフローを実行するとき

GoのTDDワークフロー

RED-GREEN-REFACTORサイクル

RED     → 最初に失敗するテストを書く
GREEN   → テストを通すための最小限のコードを書く
REFACTOR → テストが通ったままコードを改善する
REPEAT  → 次の要件に進む

Goでの段階的なTDD

// ステップ1: インターフェース/シグネチャの定義
// calculator.go
package calculator

func Add(a, b int) int {
    panic("実装されていません") // プレースホルダ
}

// ステップ2: 失敗するテストを書く (RED)
// calculator_test.go
package calculator

import "testing"

func TestAdd(t *testing.T) {
    got := Add(2, 3)
    want := 5
    if got != want {
        t.Errorf("Add(2, 3) = %d; want %d", got, want)
    }
}

// ステップ3: テストを実行 - FAILを確認
// $ go test
// --- FAIL: TestAdd (0.00s)
// panic: 実装されていません

// ステップ4: 最小限のコードを実装 (GREEN)
func Add(a, b int) int {
    return a + b
}

// ステップ5: テストを実行 - PASSを確認
// $ go test
// PASS

// ステップ6: 必要に応じてリファクタリングし、テストがまだ通ることを確認

テーブル駆動テスト

Goテストの標準パターン。最小限のコードで包括的なカバレッジを実現します。

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"正の数", 2, 3, 5},
        {"負の数", -1, -2, -3},
        {"ゼロ値", 0, 0, 0},
        {"符号混合", -1, 1, 0},
        {"大きな数", 1000000, 2000000, 3000000},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, got, tt.expected)
            }
        })
    }
}

エラーケースを含むテーブル駆動テスト

func TestParseConfig(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    *Config
        wantErr bool
    }{
        {
            name:  "有効な設定",
            input: `{"host": "localhost", "port": 8080}`,
            want:  &Config{Host: "localhost", Port: 8080},
        },
        {
            name:    "無効なJSON",
            input:   `{invalid}`,
            wantErr: true,
        },
        {
            name:    "空の入力",
            input:   "",
            wantErr: true,
        },
        {
            name:  "最小設定",
            input: `{}`,
            want:  &Config{}, // ゼロ値の設定
        },
    }

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

            if tt.wantErr {
                if err == nil {
                    t.Error("エラーが期待されましたが、nilが返されました")
                }
                return
            }

            if err != nil {
                t.Fatalf("予期しないエラー: %v", err)
            }

            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("got %+v; want %+v", got, tt.want)
            }
        })
    }
}

サブテストとサブベンチマーク

関連テストの整理

func TestUser(t *testing.T) {
    // 全サブテストで共有されるセットアップ
    db := setupTestDB(t)

    t.Run("作成", func(t *testing.T) {
        user := &User{Name: "Alice"}
        err := db.CreateUser(user)
        if err != nil {
            t.Fatalf("CreateUser failed: %v", err)
        }
        if user.ID == "" {
            t.Error("ユーザーIDが設定されている必要があります")
        }
    })

    t.Run("取得", func(t *testing.T) {
        user, err := db.GetUser("alice-id")
        if err != nil {
            t.Fatalf("GetUser failed: %v", err)
        }
        if user.Name != "Alice" {
            t.Errorf("got name %q; want %q", user.Name, "Alice")
        }
    })

    t.Run("更新", func(t *testing.T) {
        // ...
    })

    t.Run("削除", func(t *testing.T) {
        // ...
    })
}

並列サブテスト

func TestParallel(t *testing.T) {
    tests := []struct {
        name  string
        input string
    }{
        {"case1", "input1"},
        {"case2", "input2"},
        {"case3", "input3"},
    }

    for _, tt := range tests {
        tt := tt // 範囲変数をキャプチャ
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // サブテストを並列実行
            result := Process(tt.input)
            // アサーション...
            _ = result
        })
    }
}

テストヘルパー

ヘルパー関数

func setupTestDB(t *testing.T) *sql.DB {
    t.Helper() // この関数をヘルパー関数としてマーク

    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        t.Fatalf("データベースのオープンに失敗しました: %v", err)
    }

    // テスト終了時にクリーンアップ
    t.Cleanup(func() {
        db.Close()
    })

    // マイグレーションを実行
    if _, err := db.Exec(schema); err != nil {
        t.Fatalf("スキーマの作成に失敗しました: %v", err)
    }

    return db
}

func assertNoError(t *testing.T, err error) {
    t.Helper()
    if err != nil {
        t.Fatalf("予期しないエラー: %v", err)
    }
}

func assertEqual[T comparable](t *testing.T, got, want T) {
    t.Helper()
    if got != want {
        t.Errorf("got %v; want %v", got, want)
    }
}

一時ファイルとディレクトリ

func TestFileProcessing(t *testing.T) {
    // テンポラリディレクトリを作成 - 自動的にクリーンアップ
    tmpDir := t.TempDir()

    // テストファイルを作成
    testFile := filepath.Join(tmpDir, "test.txt")
    err := os.WriteFile(testFile, []byte("テストコンテンツ"), 0644)
    if err != nil {
        t.Fatalf("テストファイルの作成に失敗しました: %v", err)
    }

    // テストを実行
    result, err := ProcessFile(testFile)
    if err != nil {
        t.Fatalf("ProcessFile failed: %v", err)
    }

    // アサート...
    _ = result
}

ゴールデンファイル

testdata/に保存された期待される出力ファイルに対するテスト。

var update = flag.Bool("update", false, "ゴールデンファイルを更新")

func TestRender(t *testing.T) {
    tests := []struct {
        name  string
        input Template
    }{
        {"simple", Template{Name: "test"}},
        {"complex", Template{Name: "test", Items: []string{"a", "b"}}},
    }

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

            golden := filepath.Join("testdata", tt.name+".golden")

            if *update {
                // ゴールデンファイルを更新: go test -update
                err := os.WriteFile(golden, got, 0644)
                if err != nil {
                    t.Fatalf("ゴールデンファイルの更新に失敗しました: %v", err)
                }
            }

            want, err := os.ReadFile(golden)
            if err != nil {
                t.Fatalf("ゴールデンファイルの読み取りに失敗しました: %v", err)
            }

            if !bytes.Equal(got, want) {
                t.Errorf("出力が一致しません:\ngot:\n%s\nwant:\n%s", got, want)
            }
        })
    }
}

インターフェースを使用したモッキング

インターフェースベースのモッキング

// 依存関係のインターフェースを定義
 type UserRepository interface {
    GetUser(id string) (*User, error)
    SaveUser(user *User) error
}

// プロダクション実装
 type PostgresUserRepository struct {
    db *sql.DB
}

func (r *PostgresUserRepository) GetUser(id string) (*User, error) {
    // 実際のデータベースクエリ
}

// テスト用のモック実装
 type MockUserRepository struct {
    GetUserFunc  func(id string) (*User, error)
    SaveUserFunc func(user *User) error
}

func (m *MockUserRepository) GetUser(id string) (*User, error) {
    return m.GetUserFunc(id)
}

func (m *MockUserRepository) SaveUser(user *User) error {
    return m.SaveUserFunc(user)
}

// モックを使用したテスト
 func TestUserService(t *testing.T) {
    mock := &MockUserRepository{
        GetUserFunc: func(id string) (*User, error) {
            if id == "123" {
                return &User{ID: "123", Name: "Alice"}, nil
            }
            return nil, ErrNotFound
        },
    }

    service := NewUserService(mock)

    user, err := service.GetUserProfile("123")
    if err != nil {
        t.Fatalf("予期しないエラー: %v", err)
    }
    if user.Name != "Alice" {
        t.Errorf("got name %q; want %q", user.Name, "Alice")
    }
}

ベンチマーク

基本的なベンチマーク

func BenchmarkProcess(b *testing.B) {
    data := generateTestData(1000)
    b.ResetTimer() // セットアップ時間をカウントしない

    for i := 0; i < b.N; i++ {
        Process(data)
    }
}

// 実行: go test -bench=BenchmarkProcess -benchmem
// 出力: BenchmarkProcess-8   10000   105234 ns/op   4096 B/op   10 allocs/op

異なるサイズでのベンチマーク

func BenchmarkSort(b *testing.B) {
    sizes := []int{100, 1000, 10000, 100000}

    for _, size := range sizes {
        b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
            data := generateRandomSlice(size)
            b.ResetTimer()

            for i := 0; i < b.N; i++ {
                // ソート済みデータのソートを避けるためにコピーを作成
                tmp := make([]int, len(data))
                copy(tmp, data)
                sort.Ints(tmp)
            }
        })
    }
}

メモリアロケーションベンチマーク

func BenchmarkStringConcat(b *testing.B) {
    parts := []string{"hello", "world", "foo", "bar", "baz"}

    b.Run("plus", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var s string
            for _, p := range parts {
                s += p
            }
            _ = s
        }
    })

    b.Run("builder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var sb strings.Builder
            for _, p := range parts {
                sb.WriteString(p)
            }
            _ = sb.String()
        }
    })

    b.Run("join", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = strings.Join(parts, "")
        }
    })
}

ファジング (Go 1.18以降)

基本的なファズテスト

func FuzzParseJSON(f *testing.F) {
    // シードコーパスを追加
    f.Add(`{"name": "test"}`)
    f.Add(`{"count": 123}`)
    f.Add(`[]`)
    f.Add(`""`)

    f.Fuzz(func(t *testing.T, input string) {
        var result map[string]interface{}
        err := json.Unmarshal([]byte(input), &result)

        if err != nil {
            // ランダム入力では無効なJSONが期待される
            return
        }

        // パースが成功した場合、再エンコードが機能するはず
        _, err = json.Marshal(result)
        if err != nil {
            t.Errorf("Unmarshal成功後にMarshalが失敗しました: %v", err)
        }
    })
}

// 実行: go test -fuzz=FuzzParseJSON -fuzztime=30s

複数の入力を持つファズテスト

func FuzzCompare(f *testing.F) {
    f.Add("hello", "world")
    f.Add("", "")
    f.Add("abc", "abc")

    f.Fuzz(func(t *testing.T, a, b string) {
        result := Compare(a, b)

        // プロパティ: Compare(a, a) は常に0を返すべき
        if a == b && result != 0 {
            t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result)
        }

        // プロパティ: Compare(a, b) と Compare(b, a) は逆の符号を持つべき
        reverse := Compare(b, a)
        if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) {
            if result != 0 || reverse != 0 {
                t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; 一貫性がありません",
                    a, b, result, b, a, reverse)
            }
        }
    })
}

テストカバレッジ

カバレッジの実行

# 基本的なカバレッジ
go test -cover ./...

# カバレッジプロファイルの生成
go test -coverprofile=coverage.out ./...

# ブラウザでカバレッジを表示
go tool cover -html=coverage.out

# 関数ごとのカバレッジを表示
go tool cover -func=coverage.out

# レース検出付きカバレッジ
go test -race -coverprofile=coverage.out ./...

カバレッジ目標

コードタイプ目標
重要なビジネスロジック100%
公開API90%以上
一般的なコード80%以上
生成コード除外

生成コードをカバレッジから除外

//go:generate mockgen -source=interface.go -destination=mock_interface.go

// カバレッジプロファイルでビルドタグを使用して除外:
// go test -cover -tags=!generate ./...

HTTPハンドラテスト

func TestHealthHandler(t *testing.T) {
    // リクエストを作成
    req := httptest.NewRequest(http.MethodGet, "/health", nil)
    w := httptest.NewRecorder()

    // ハンドラを呼び出し
    HealthHandler(w, req)

    // レスポンスを確認
    resp := w.Result()
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK)
    }

    body, _ := io.ReadAll(resp.Body)
    if string(body) != "OK" {
        t.Errorf("got body %q; want %q", body, "OK")
    }
}

func TestAPIHandler(t *testing.T) {
    tests := []struct {
        name       string
        method     string
        path       string
        body       string
        wantStatus int
        wantBody   string
    }{
        {
            name:       "ユーザーを取得",
            method:     http.MethodGet,
            path:       "/users/123",
            wantStatus: http.StatusOK,
            wantBody:   `{"id":"123","name":"Alice"}`,
        },
        {
            name:       "見つかりません",
            method:     http.MethodGet,
            path:       "/users/999",
            wantStatus: http.StatusNotFound,
        },
        {
            name:       "ユーザーを作成",
            method:     http.MethodPost,
            path:       "/users",
            body:       `{"name":"Bob"}`,
            wantStatus: http.StatusCreated,
        },
    }

    handler := NewAPIHandler()

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // リクエストを作成
            var body io.Reader
            if tt.body != "" {
                body = strings.NewReader(tt.body)
            }
            req := httptest.NewRequest(tt.method, tt.path, body)
            req.Header.Set("Content-Type", "application/json")
            w := httptest.NewRecorder()

            // ハンドラを呼び出し
            handler.ServeHTTP(w, req)

            // レスポンスを確認
            resp := w.Result()
            defer resp.Body.Close()

            if resp.StatusCode != tt.wantStatus {
                t.Errorf("got status %d; want %d", resp.StatusCode, tt.wantStatus)
            }

            if tt.wantBody != "" {
                bodyBytes, _ := io.ReadAll(resp.Body)
                if string(bodyBytes) != tt.wantBody {
                    t.Errorf("got body %q; want %q", string(bodyBytes), tt.wantBody)
                }
            }
        })
    }
}

構文的なイディオム

テスト名の命名

// 良い命名
func TestUserCanLogin(t *testing.T) { /* ... */ }
func TestUserLoginWithInvalidPassword(t *testing.T) { /* ... */ }
func TestUserLogin_WhenUserIsLocked(t *testing.T) { /* ... */ }

// 避けるべき命名
func TestUser1(t *testing.T) { /* ... */ }
func TestFunctionality(t *testing.T) { /* ... */ }
func TestMyNewFeature(t *testing.T) { /* ... */ }

TestMain

func TestMain(m *testing.M) {
    // 前処理
    setup()

    // テストを実行
    code := m.Run()

    // 後処理
    teardown()

    // 終了
    os.Exit(code)
}

エラー vs 致命的エラー

t.Error("このテストケースは失敗しました") // 残りのサブテストを続行
t.Fatalf("致命的なエラー: %v", err)    // このサブテストを即座に停止
t.Fatal("データベースに接続できません")   // テストを完全に中止

// Fatalf は Fatal と同等 (フォーマット指定子付き)

データ駆動テスト (testdata ディレクトリ)

func TestParseFiles(t *testing.T) {
    entries, err := os.ReadDir("testdata")
    if err != nil {
        t.Fatalf("testdata ディレクトリを読み取れません: %v", err)
    }

    for _, entry := range entries {
        if entry.IsDir() {
            continue
        }

        t.Run(entry.Name(), func(t *testing.T) {
            data, err := os.ReadFile(filepath.Join("testdata", entry.Name()))
            if err != nil {
                t.Fatalf("ファイル %s を読み取れません: %v", entry.Name(), err)
            }

            result, err := ParseConfig(string(data))
            if err != nil {
                t.Fatalf("ファイル %s のパースに失敗しました: %v", entry.Name(), err)
            }

            // 結果をアサート...
            _ = result
        })
    }
}

クイックリファレンス

コマンド説明
go test現在のパッケージのテストを実行
go test ./...全パッケージのテストを実行
go test -v詳細な出力付きでテストを実行
go test -run TestName特定のテストのみを実行
go test -bench=.すべてのベンチマークを実行
go test -coverカバレッジ付きでテストを実行
go test -fuzz=FuzzName特定のファズテストを実行
go test -raceレース検出器を有効化
go test -count=1キャッシュを無効化(クリーンラン)
go test -timeout=30sタイムアウトを設定
go test -short長時間実行テストをスキップ
go tool cover -html=c.outHTML形式でカバレッジレポートを生成

ベストプラクティス

  1. テストを冪等にする - 各テストは独立して実行可能でなければなりません
  2. テストに名前を付ける - テーブル駆動テストでは説明的な名前を使用する
  3. t.Helper() を使用する - ヘルパー関数のエラーが正しい行を指すようにする
  4. t.Cleanup() を優先する - クリーンアップをセットアップの近くに配置し、defer よりも推奨
  5. t.TempDir() を使用する - テストファイルの自動クリーンアップ
  6. 避けるべきもの: テストの共有状態、ネットワークコール(単体テスト)、ファイルシステムへの副作用、データベース接続(単体テスト)
  7. テストはアサーションで終了させること - テストの最後にアサーションがない場合は避ける
  8. テーブル駆動テストを使用する - 同じロジックの複数の類似テストを避ける
  9. 本番コードをテスト用に変更しない - テスト可能性のために公開範囲を広げない(代わりに internal テストパッケージを使用する)
  10. エクスポートされていない関数をテストする - 必要な場合は _test パッケージを使用するのではなく、同じパッケージ内でテストする

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

関連スキル