go-testing-code-review について
go-testing-code-reviewは、Goのテストや関連変更を評価するレビュー向けスキルです。Goのバージョン確認、周辺テストの読解、該当チェック項目の適用、報告前の検証を順に促します。
主な機能
- 01順序を定めたレビュー手順
- 02Goバージョンに応じたテスト指針
- 03テスト構成とカバレッジの確認項目
- 04モックとテスト分離の指針
- 05重大度を含む指摘フォーマット
活用シーン
- 01Goテストファイルの変更レビュー
- 02並列テストの共有状態の確認
- 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 テストのコードレビュー
レビューワークフロー
以下の手順を順番どおりに実行してください。以下のすべてのパス条件を満たすまで、指摘事項を出力しないでください。
-
go.modの基準確認 — レビュー対象のモジュールのgo.modを開き、goディレクティブを確認します。
パス条件: 正確なgo X.YYの値を(レビュー冒頭または作業メモに)記載できます。この基準バージョンに一致する場合にのみ、バージョンによって適用可否が変わるアドバイスを適用してください(例:ファズテストは Go 1.18 以降、ループ変数のキャプチャは Go 1.22 より前)。 -
周辺のテストを読む — 対象範囲内の各
*_test.go(またはベンチマーク/ファズテストファイル)について、差分部分だけでなく、テスト関数全体と、そのテストが使用するテーブルのstruct{...}やヘルパーも読みます。
パス条件: 対象範囲内の各ファイルについて、変更を含むfunc Test.../func Benchmark.../func Fuzz...全体(またはそれが呼び出すヘルパー)を少なくとも1つ読みます。 -
チェックリストの対象範囲を決める — レビュー用チェックリストのどの項目が該当するかを判断します(テーブル駆動の構造、並列実行、HTTP、ゴールデンファイル、モック)。該当する項目については references/structure.md および/または references/mocking.md を開きます。差分に該当しない項目は、1行で理由を示して対象外にします(例:「変更箇所に
t.Parallelがない」)。
パス条件: レビュー(または作業メモ)に、適用したチェックリストのテーマを記載するか、差分に即した理由とともに対象外のテーマを示します。 -
報告前の検証 — 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
レビュー時の質問
- テストは名前付きケースを使ったテーブル駆動になっていますか?
- エラーメッセージには入力、got、want が含まれていますか?
- 並列テストは分離されていますか(状態を共有していませんか)?
- クリーンアップは t.Cleanup で行われていますか?
- テストは実装ではなく動作を検証していますか?