mcpField
G
golang-testing

golang-testing

github.com/samberテスト・品質3.7k43.9k 回インストール5 日前に更新
リポジトリ

golang-testing について

golang-testingは、Goテストの設計、作成、レビュー、監査、デバッグを支援するスキルです。動作の検証、分離性、決定性、テストスイートの保守性を重視し、作業別の手順を備えています。

主な機能

  1. 01作成・レビュー・監査・デバッグの各モード
  2. 02名前付きテーブル駆動テストの指針
  3. 03ビルドタグによる統合テストの分離
  4. 04並行処理とゴルーチンリークの確認
  5. 05ファジング・ベンチマーク・カバレッジの指針

活用シーン

  1. 01新しいGoの動作に対するテスト作成
  2. 02プルリクエストのテスト変更レビュー
  3. 03テストスイートの不足や不安定さの監査
  4. 04失敗または不安定なテストの原因調査

インストール

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

$npx skills add https://github.com/samber/cc-skills-golang --skill golang-testing

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
evals.json
references/
benchmarks.md
coverage.md
examples.md
helpers.md
http-testing.md
integration-testing.md
mocking.md

詳細

ペルソナ: テストを実行可能な仕様として捉える Go エンジニアです。カバレッジ目標を達成するためではなく、動作を制約するためにテストを書きます。

思考モード: テスト戦略の設計と失敗分析について、できる限り徹底的に考えてください。浅い思考ではエッジケースを見落とし、今は通っても後で壊れる脆弱なテストが生まれます。Claude Code では、ultrathink を使って拡張思考を明示的に開始してください。

オーケストレーションモード: 大規模なテストスイートを監査する場合は、Audit モードで説明されている 3 つのサブエージェント(ユニットテストの品質とカバレッジの不足、統合テストの分離、ゴルーチンと競合の問題)に作業を分担し、調査結果を 1 つのギャップレポートにまとめてください。Claude Code では、ultracode を使ってマルチエージェントオーケストレーションを明示的に有効にしてください。

モード:

  • Write モード — 既存コードまたは新規コードのテストを作成します。テスト対象のコードを順に確認し、gotests を使ってテーブル駆動テストのひな形を作成してから、エッジケースとエラー処理を追加します。
  • Review モード — PR のテスト変更をレビューします。差分に注目し、新しい動作のカバレッジ、アサーションの品質、テーブル駆動テストの構成、不安定さにつながるパターンがないことを確認します。順番に進めます。
  • Audit モード — 既存のテストスイートにある不足、不安定さ、望ましくないパターン(実行順序に依存するテスト、t.Parallel() の不足、実装詳細への結合)を監査します。関心事ごとに最大 3 つの並列サブエージェントを起動します:(1)ユニットテストの品質とカバレッジの不足、(2)統合テストの分離とビルドタグ、(3)ゴルーチンリークとデータ競合。
  • Debug モード — テストが失敗する、または不安定な場合に使います。順番に進め、失敗を確実に再現し、失敗するアサーションを切り分け、本番コードまたはテストのセットアップを追跡して根本原因を特定します。

コミュニティのデフォルト。 samber/cc-skills-golang@golang-testing スキルを明示的に置き換える会社のスキルが優先されます。

依存関係:

  • gotests:go install github.com/cweill/gotests/gotests@latest

Go テストのベストプラクティス

このスキルでは、Go アプリケーション向けの本番環境対応テストの作成方法を説明します。保守しやすく、高速で、信頼性の高いテストを書くために、以下の原則に従ってください。

ベストプラクティスの概要

  1. テーブル駆動テストでは、名前付きサブテストを必ず使用してください。各テストケースに name フィールドを設け、t.Run に渡す必要があります。
  2. 統合テストでは、単体テストと分離するためにビルドタグ(//go:build integration)を必ず使用してください。
  3. テストは実行順序に依存してはなりません。各テストは単独で実行できる必要があります。
  4. 独立したテストでは、可能な場合に t.Parallel() を使用してください。
  5. テストでは、実装詳細ではなく、観測可能な動作と公開 API の契約を必ず検証してください。内部実装に結合したテストは、契約について何も証明しないまま、リファクタリングのたびにテストの書き直しを招きます。
  6. ゴルーチンを使うパッケージでは、ゴルーチンリークを検出するため、TestMain で goleak.VerifyTestMain を使用してください。
  7. testify はヘルパーとして使い、標準ライブラリの代わりにしないでください。
  8. 具象型ではなく、インターフェースをモックしてください。
  9. 単体テストは高速に保ち(< 1ms)、統合テストにはビルドタグを使用してください。
  10. CI ではレース検出を有効にしてテストを実行してください。
  11. 実行可能なドキュメントとして、サンプルを含めてください。
  12. テストファイル名には、テスト対象の関数やメソッドではなく、テスト対象のソースファイル名を必ず使ってください。
  13. テスト関数は、ソースファイル内のテスト対象の関数やメソッドと同じ順序で配置してください。

テストの構成と整理

ファイルの規約
// package_test.go - tests in same package (white-box, access unexported)
package mypackage

// mypackage_test.go - tests in test package (black-box, public API only)
package mypackage_test

テストする関数やメソッドではなく、テスト対象のソースファイルにちなんでテストファイルに名前を付けます。Go では、ソースファイルごとにテストファイルを 1 つ作るのが慣例です(foo.go -> foo_test.go)。ツール(go test、カバレッジレポート、IDE の「テストへ移動」ナビゲーション、gotests)もレビュー担当者も、シンボルではなくソースファイルを基準にテストを特定するためです。通常、1 つのソースファイルには複数の関数やメソッドが宣言されています。それぞれのシンボル名ごとにテストを分割すると、多数のファイルに分散し、ソースとテストのファイル間の対応関係が崩れます。

// ✓ Good — one test file per source file
helloworld.go       -> helloworld_test.go   // contains TestHelloWorld, TestAbcd, TestXyz, ...

// ✗ Bad — test file named after the function/method instead of the source file
helloworld.go       -> abcd_test.go         // wrong: should be helloworld_test.go

例外として、非常に大きなソースファイルは、関心事ごとに複数の _test.go ファイルに分割しても構いません(例:foo_test.go + foo_edgecases_test.go)。ただし、分割した各ファイルの名前は、個々の関数名ではなく、必ずソースファイル名に由来するものにします。ファイルが大きくなっても、ソースファイルごとに単一の _test.go ファイルを維持することを優先してください。分割するとナビゲーションの負担が増え、それに見合う価値があることはまれです。単一ファイルの閲覧やレビューが本当に困難になった場合に限り、この例外を適用してください。

テストファイル内では、テスト関数を、テスト対象の関数やメソッドがソースファイルに現れる順序に合わせて並べます。foo.go と foo_test.go を並べてスクロールする読者(人間でもエージェントでも)は、検索せずに位置から対応するテストを見つけられます。どちらかのファイルが大きくなるたびに、2 つの並び順のずれが積み重なります。

命名規約
func TestAdd(t *testing.T) { ... }               // function test
func TestMyStruct_MyMethod(t *testing.T) { ... } // method test
func BenchmarkAdd(b *testing.B) { ... }          // benchmark
func ExampleAdd() { ... }                        // example
func FuzzAdd(f *testing.F) { ... }               // fuzz test

テーブル駆動テスト

テーブル駆動テストは、複数のシナリオをテストするための Go で慣用的な方法です。各テストケースには必ず名前を付けてください。

func TestCalculatePrice(t *testing.T) {
    tests := []struct {
        name     string
        quantity int
        unitPrice float64
        expected  float64
    }{
        {
            name:      "single item",
            quantity:  1,
            unitPrice: 10.0,
            expected:  10.0,
        },
        {
            name:      "bulk discount - 100 items",
            quantity:  100,
            unitPrice: 10.0,
            expected:  900.0, // 10% discount
        },
        {
            name:      "zero quantity",
            quantity:  0,
            unitPrice: 10.0,
            expected:  0.0,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := CalculatePrice(tt.quantity, tt.unitPrice)
            if got != tt.expected {
                t.Errorf("CalculatePrice(%d, %.2f) = %.2f, want %.2f",
                    tt.quantity, tt.unitPrice, got, tt.expected)
            }
        })
    }
}

よくある落とし穴:サブテストに漏れ出すアサーションのスコープ

親テスト関数で testify の assert/require インスタンスを作成し、t.Run のクロージャ内で再利用してはいけません。assert.New(t) は、作成時に渡された *testing.T をそのまま保持します。その t が親テストのものだと、サブテスト内で発生したすべての失敗が go test の出力で_親_テストに紐付けられます。失敗したサブテスト自体は --- PASS と報告されるため、どのケースで問題が起きたのかが見えなくなります。サブテストが t.Parallel() を呼び出すかどうかにかかわらず、これは発生します。

// WRONG -- `is` is bound to the parent's t
func TestCalculatePrice(t *testing.T) {
    is := assert.New(t)
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            is.Equal(tt.expected, CalculatePrice(tt.quantity, tt.unitPrice)) // misattributed on failure
        })
    }
}

// RIGHT -- each subtest builds its own instance from its own t
func TestCalculatePrice(t *testing.T) {
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            is := assert.New(t)
            is.Equal(tt.expected, CalculatePrice(tt.quantity, tt.unitPrice))
        })
    }
}

意図的に失敗するケースで確認してください。go test -v -run TestName の実行結果に --- FAIL: TestName が表示される一方、すべての --- PASS: TestName/subtest_name 行が PASS のままであれば、アサーションのスコープが漏れています。

単体テスト

単体テストは高速(< 1ms)で、独立しており(外部依存なし)、決定的であるべきです。

HTTP ハンドラーのテスト

ハンドラーのテストでは、テーブル駆動のパターンと httptest を使用します。リクエスト/レスポンスの本文、クエリパラメーター、ヘッダー、ステータスコードのアサーションの例については、HTTP Testing を参照してください。

goleak によるゴルーチンリークの検出

特に並行コードでは、リークするゴルーチンを検出するために go.uber.org/goleak を使用します。

import (
    "testing"
    "go.uber.org/goleak"
)

func TestMain(m *testing.M) {
    goleak.VerifyTestMain(m)
}

既知のリークやライブラリのゴルーチンなど、特定のゴルーチンスタックを除外するには、次のようにします。

func TestMain(m *testing.M) {
    goleak.VerifyTestMain(m,
        goleak.IgnoreCurrent(),
    )
}

または、テストごとに設定します。

func TestWorkerPool(t *testing.T) {
    defer goleak.VerifyNone(t)
    // ... test code ...
}

決定的なゴルーチンテストのための testing/synctest

testing/synctest(Go 1.25 以降)では、ゴルーチン、タイマー、デッドライン、コンテキストのキャンセルを決定的にテストできます。すべてのゴルーチンがブロックされているときにのみ時間が進むため、実行順序を予測しやすくなります。

実時間の代わりに synctest を使う場面:

  • 時間に基づく操作(time.Sleep、time.After、time.Ticker)を伴う並行コードをテストする場合
  • 競合状態を再現可能にする必要がある場合
  • タイミングの問題によってテストが不安定になる場合
import (
    "context"
    "testing"
    "testing/synctest"
    "time"
)

func TestContextTimeout(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        const timeout = 5 * time.Second

        ctx, cancel := context.WithTimeout(t.Context(), timeout)
        defer cancel()

        time.Sleep(timeout - time.Nanosecond)
        synctest.Wait()
        if err := ctx.Err(); err != nil {
            t.Fatalf("before timeout: %v", err)
        }

        time.Sleep(time.Nanosecond)
        synctest.Wait()
        if err := ctx.Err(); err != context.DeadlineExceeded {
            t.Fatalf("after timeout: got %v, want DeadlineExceeded", err)
        }
    })
}

Go 1.25 以降では synctest.Test を使用してください。Go 1.25 以降のコードでは、古い Go 1.24 の実験的な synctest.Run API を使用しないでください。モジュールが Go 1.24 を明示的に対象とし、GOEXPERIMENT=synctest を有効にしている場合に限り、互換性のためのフォールバックとして古い API を使用してください。

synctest の主な違い:

  • ゴルーチンがブロックすると、time.Sleep によって合成時間が即座に進みます
  • 合成時間が指定した期間に達すると、time.After が発火します
  • 時間が進む前に、すべてのゴルーチンがブロック地点まで実行されます
  • テストの実行は決定的で、繰り返し可能です
  • Go 1.27 以降では、バブルの偽時計を進める直接的なヘルパーとして synctest.Sleep(d) が追加されます。これは time.Sleep(d) の後に synctest.Wait() を呼び出すのと同等ですが、ブロックする実際のゴルーチンは必要ありません

Go 1.27 以降では、メモリ内の偽ネットワーク版 httptest.NewServer である httptest.NewTestServer() も追加されており、synctest と組み合わせて使えます。実際のソケットを使わないため、サーバーのテストは httptest.NewServer と実際のタイマーを使う代わりに、synctest.Test のバブル内で実行できます。

テストのタイムアウト

ハングする可能性のあるテストには、呼び出し元の位置を示してパニックするタイムアウトヘルパーを使用してください。ヘルパーを参照してください。

ベンチマーク

ベンチマークはサブベンチマークとして(バリエーションごとに b.Run を使って)記述し、各バリエーションに出力上で固有の名前が付くようにしてください。この名前が比較ツールで差分を取る対象になります。Go 1.24 以降では、b.N ループではなく b.Loop() を使用してください。

→ コードの書き方やサイズをパラメーター化した例については、テストスイートでのベンチマークを参照してください。

→ 測定方法(benchstat、ベンチマークからのプロファイリング、CI での回帰検出)については、samber/cc-skills-golang@golang-benchmark スキルを参照してください。

Go 1.26 以降:テスト成果物

テスト、ベンチマーク、ファズターゲットでファイルを保存して調査する必要がある場合は、その場しのぎのパスやリポジトリ内の出力先ではなく、ArtifactDir() を使用してください。

func TestRenderGoldenArtifact(t *testing.T) {
    dir := t.ArtifactDir()

    out := filepath.Join(dir, "rendered.json")
    if err := os.WriteFile(out, renderedBytes, 0o644); err != nil {
        t.Fatal(err)
    }

    t.Logf("artifact written: %s", out)
}

Go 1.26 以降では、*testing.T、*testing.B、*testing.F で利用できます。

Go 1.27 以降:stdversion の自動実行

go test は stdversion の vet チェックをデフォルトで実行するようになり、モジュールの go ディレクティブより新しい API の使用を検出します。このチェックによる CI の失敗は、go ディレクティブを引き上げるか、新しい API の使用をやめる必要があることを意味します。無視するためのチェックではありません。

並列テスト

t.Parallel() を使ってテストを並行実行します。

func TestParallelOperations(t *testing.T) {
    tests := []struct {
        name string
        data []byte
    }{
        {"small data", make([]byte, 1024)},
        {"medium data", make([]byte, 1024*1024)},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            is := assert.New(t)

            result := Process(tt.data)
            is.NotNil(result)
        })
    }
}

ファジング

ファジングを使って、エッジケースやバグを見つけます。

func FuzzReverse(f *testing.F) {
    f.Add("hello")
    f.Add("")
    f.Add("a")

    f.Fuzz(func(t *testing.T, input string) {
        reversed := Reverse(input)
        doubleReversed := Reverse(reversed)
        if input != doubleReversed {
            t.Errorf("Reverse(Reverse(%q)) = %q, want %q", input, doubleReversed, input)
        }
    })
}

ドキュメントとしてのサンプル

ExampleXxx 関数は実行可能なドキュメントです。go test は標準出力を // Output: コメントと比較するため、内容が古くなったサンプルは読者を誤解させるのではなく、ビルドを失敗させます。

→ 命名規則、Unordered output、配置については、ドキュメントとしてのサンプルを参照してください。

コードカバレッジ

go test -coverprofile=coverage.out ./... でプロファイルを生成し、go tool cover -html=coverage.out でカバーされていない行を確認します。カバレッジはテストされていない経路を特定するものであり、アサーションの品質を測るものではありません。そのため、パーセンテージは目標ではなく、抜けを見つけるためのものとして扱ってください。

→ カバレッジモード、-coverpkg、レポート作成時の注意点については、コードカバレッジを参照してください。

統合テスト

ビルドタグを使って、統合テストと単体テストを分けます。

//go:build integration

package mypackage

func TestDatabaseIntegration(t *testing.T) {
    db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close()

    // Test real database operations
}

統合テストを個別に実行します。

go test -tags=integration ./...

Docker Compose のフィクスチャ、SQL スキーマ、統合テストスイートについては、統合テストを参照してください。

モック

具象型ではなく、インターフェースをモックします。使用する側でインターフェースを定義し、モック実装を作成します。

モックのパターン、テスト用フィクスチャ、時間のモックについては、モックを参照してください。

リンターで強制する

テストのベストプラクティスの多くは、thelper、paralleltest、testifylint などのリンターで自動的に強制されます。設定と使い方については、samber/cc-skills-golang@golang-lint スキルを参照してください。

関連項目

  • → testify API(assert、require、mock、suite)の詳細については、samber/cc-skills-golang@golang-stretchr-testify スキルを参照してください
  • → データベース統合テストのパターンについては、samber/cc-skills-golang@golang-database スキル(testing.md)を参照してください
  • → goleak を使ったゴルーチンのリーク検出については、samber/cc-skills-golang@golang-concurrency スキルを参照してください
  • → CI のテスト設定と GitHub Actions のワークフローについては、samber/cc-skills-golang@golang-continuous-integration スキルを参照してください
  • → testifylint と paralleltest の設定については、samber/cc-skills-golang@golang-lint スキルを参照してください
  • → これらのガイドラインを用いた、CI での AI による自動コードレビューについては、samber/cc-skills-golang@golang-continuous-integration スキルを参照してください

クイックリファレンス

go test ./...                          # all tests
go test -run TestName ./...            # specific test by exact name
go test -run TestName/subtest ./...    # subtests within a test
go test -run 'Test(Add|Sub)' ./...     # multiple tests (regexp OR)
go test -run 'Test[A-Z]' ./...         # tests starting with capital letter
go test -run 'TestUser.*' ./...        # tests matching prefix
go test -run '.*Validation.*' ./...    # tests containing substring
go test -run TestName/. ./...          # all subtests of TestName
go test -run '/(unit|integration)' ./... # filter by subtest name
go test -race ./...                    # race detection
go test -cover ./...                   # coverage summary
go test -bench=. -benchmem ./...       # benchmarks
go test -fuzz=FuzzName ./...           # fuzzing
go test -tags=integration ./...        # integration tests

コメント