mcpField
G
go-testing-code-review

go-testing-code-review

github.com/existential-birdsテスト・品質85143 回インストール11 週間前に更新
リポジトリ

go-testing-code-review について

go-testing-code-reviewは、Goのテストや関連変更を評価するレビュー向けスキルです。Goのバージョン確認、周辺テストの読解、該当チェック項目の適用、報告前の検証を順に促します。

主な機能

  1. 01順序を定めたレビュー手順
  2. 02Goバージョンに応じたテスト指針
  3. 03テスト構成とカバレッジの確認項目
  4. 04モックとテスト分離の指針
  5. 05重大度を含む指摘フォーマット

活用シーン

  1. 01Goテストファイルの変更レビュー
  2. 02並列テストの共有状態の確認
  3. 03HTTP、ファジング、ベンチマーク、ゴールデンファイルテストの評価

インストール

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

$npx skills add https://github.com/existential-birds/beagle --skill go-testing-code-review

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

同梱ファイルreferences/mocking.mdreferences/structure.md

詳細

Go テストのコードレビュー

レビューワークフロー

以下の手順を順番どおりに実行してください。以下のすべてのパス条件を満たすまで、指摘事項を出力しないでください。

  1. go.mod の基準確認 — レビュー対象のモジュールの go.mod を開き、go ディレクティブを確認します。
    パス条件: 正確な go X.YY の値を(レビュー冒頭または作業メモに)記載できます。この基準バージョンに一致する場合にのみ、バージョンによって適用可否が変わるアドバイスを適用してください(例:ファズテストは Go 1.18 以降、ループ変数のキャプチャは Go 1.22 より前)。

  2. 周辺のテストを読む — 対象範囲内の各 *_test.go(またはベンチマーク/ファズテストファイル)について、差分部分だけでなく、テスト関数全体と、そのテストが使用するテーブルの struct{...} やヘルパーも読みます。
    パス条件: 対象範囲内の各ファイルについて、変更を含む func Test.../func Benchmark.../func Fuzz... 全体(またはそれが呼び出すヘルパー)を少なくとも1つ読みます。

  3. チェックリストの対象範囲を決める — レビュー用チェックリストのどの項目が該当するかを判断します(テーブル駆動の構造、並列実行、HTTP、ゴールデンファイル、モック)。該当する項目については references/structure.md および/または references/mocking.md を開きます。差分に該当しない項目は、1行で理由を示して対象外にします(例:「変更箇所に t.Parallel がない」)。
    パス条件: レビュー(または作業メモ)に、適用したチェックリストのテーマを記載するか、差分に即した理由とともに対象外のテーマを示します。

  4. 報告前の検証 — review-verification-protocol を読み込み、手順に従います。
    パス条件: 報告する各指摘について、プロトコルの報告前検証チェックリストを満たします(実際のテストコードを読み、周辺のコンテキストを確認し、「誤り」と「スタイルの違い」を区別するなど)。

必須ゲート(同じ手順を簡潔に記載)

ステップ目標とするパス条件
1バージョン固有のテストに関するアドバイスをする前に、go.mod の go X.YY を記録します。
2対象範囲内の各テストファイルについて、差分だけでなく、テスト全体(またはそのテストが使用するヘルパー)を読みます。
3対象範囲内のチェックリストのテーマを記載するか、差分に即した理由とともに対象外とします。必要に応じて参考資料を開きます。
4報告するすべての問題について、review-verification-protocol を完了します。

出力形式

指摘事項は次の形式で報告します。

[FILE:LINE] ISSUE_TITLE
Severity: Critical | Major | Minor | Informational
Description of the issue and why it matters.

クイックリファレンス

問題の種類参考資料
テストの構造、命名references/structure.md
モック、インターフェースreferences/mocking.md

レビューチェックリスト

  • テストは明確なケース名を持つテーブル駆動になっている
  • サブテストは並列実行に t.Run を使用している
  • テスト名は実装ではなく動作を表している
  • エラーには got/want と説明的なメッセージが含まれている
  • クリーンアップは t.Cleanup に登録されている
  • 並列テスト間で可変状態を共有していない
  • モックはテストファイル内で定義されたインターフェースを使用している
  • エッジケースとエラーパスをカバーしている
  • パフォーマンスが重要な関数には Benchmark* テストがある
  • 入力パーサー/バリデーターには Fuzz* テストがある(Go 1.18 以降)
  • HTTP ハンドラーは httptest.NewRequest/httptest.NewRecorder でテストされている
  • ゴールデンファイルテストは testdata/*.golden パターンと -update フラグを使用している

重要なパターン

テーブル駆動テスト
// BAD - repetitive
func TestAdd(t *testing.T) {
    if Add(1, 2) != 3 {
        t.Error("wrong")
    }
    if Add(0, 0) != 0 {
        t.Error("wrong")
    }
}

// GOOD
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        want     int
    }{
        {"positive numbers", 1, 2, 3},
        {"zeros", 0, 0, 0},
        {"negative", -1, 1, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}
エラーメッセージ
// BAD
if got != want {
    t.Error("wrong result")
}

// GOOD
if got != want {
    t.Errorf("GetUser(%d) = %v, want %v", id, got, want)
}

// For complex types
if diff := cmp.Diff(want, got); diff != "" {
    t.Errorf("GetUser() mismatch (-want +got):\n%s", diff)
}
並列テスト
func TestFoo(t *testing.T) {
    tests := []struct{...}

    for _, tt := range tests {
        tt := tt  // capture (not needed Go 1.22+)
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            // test code
        })
    }
}
クリーンアップ
// BAD - manual cleanup, skipped on failure
func TestWithTempFile(t *testing.T) {
    f, _ := os.CreateTemp("", "test")
    defer os.Remove(f.Name())  // skipped if test panics
}

// GOOD
func TestWithTempFile(t *testing.T) {
    f, _ := os.CreateTemp("", "test")
    t.Cleanup(func() {
        os.Remove(f.Name())
    })
}

その他のパターン

ベンチマーク
func BenchmarkProcess(b *testing.B) {
    data := generateTestData(1000)
    b.ResetTimer()

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

// Run: go test -bench=BenchmarkProcess -benchmem
ファズテスト(Go 1.18 以降)
func FuzzParseInput(f *testing.F) {
    // Seed corpus
    f.Add(`{"name": "test"}`)
    f.Add(``)
    f.Add(`{invalid}`)

    f.Fuzz(func(t *testing.T, input string) {
        result, err := ParseInput(input)
        if err != nil {
            return // invalid input is expected
        }
        // If parsing succeeded, re-encoding should work
        if _, err := json.Marshal(result); err != nil {
            t.Errorf("Marshal after Parse: %v", err)
        }
    })
}

// Run: go test -fuzz=FuzzParseInput -fuzztime=30s
HTTP ハンドラーテスト
func TestHandler(t *testing.T) {
    srv := NewServer(mockDeps)

    req := httptest.NewRequest("GET", "/api/users/123", nil)
    w := httptest.NewRecorder()

    srv.ServeHTTP(w, req)

    if w.Code != http.StatusOK {
        t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
    }
}
ゴールデンファイル
var update = flag.Bool("update", false, "update golden files")

func TestRender(t *testing.T) {
    got := Render(input)
    golden := filepath.Join("testdata", t.Name()+".golden")

    if *update {
        if err := os.WriteFile(golden, got, 0644); err != nil {
            t.Fatalf("writing golden file: %v", err)
        }
    }

    want, err := os.ReadFile(golden)
    if err != nil {
        t.Fatalf("reading golden file: %v (run with -update to create)", err)
    }
    if !bytes.Equal(got, want) {
        t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)
    }
}

アンチパターン

1. 内部実装をテストする
// BAD - tests private state
func TestUser(t *testing.T) {
    u := NewUser("alice")
    if u.id != 1 {  // testing internal field
        t.Error("wrong id")
    }
}

// GOOD - tests behavior
func TestUser(t *testing.T) {
    u := NewUser("alice")
    if u.ID() != 1 {
        t.Error("wrong ID")
    }
}
2. 可変状態を共有する
// BAD - tests interfere with each other
var testDB = setupDB()

func TestA(t *testing.T) {
    t.Parallel()
    testDB.Insert(...)  // race!
}

// GOOD - isolated per test
func TestA(t *testing.T) {
    db := setupTestDB(t)
    t.Cleanup(func() { db.Close() })
    db.Insert(...)
}
3. コンテキストのないアサーション
// BAD
assert.Equal(t, want, got)  // "expected X got Y" - which test?

// GOOD
assert.Equal(t, want, got, "user name after update")

参考資料を読み込むタイミング

  • テストファイルの構造をレビューする場合 → structure.md
  • モックの実装をレビューする場合 → mocking.md

レビュー時の質問

  1. テストは名前付きケースを使ったテーブル駆動になっていますか?
  2. エラーメッセージには入力、got、want が含まれていますか?
  3. 並列テストは分離されていますか(状態を共有していませんか)?
  4. クリーンアップは t.Cleanup で行われていますか?
  5. テストは実装ではなく動作を検証していますか?

コメント