mcpField
G
go-table-driven-tests

go-table-driven-tests

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

go-table-driven-tests について

Go のテーブル駆動テストを作成・変更する開発者向けのスキルです。テストケースやサブテスト、ヘルパーのパターンに加え、環境の後処理や認証情報がない場合の統合テストのスキップも扱います。

主な機能

  1. 01テーブルでケースを整理
  2. 02t.Run() で名前付きケースを実行
  3. 03期待値とエラーを検証
  4. 04テスト環境を設定・後処理
  5. 05認証情報がなければ統合テストをスキップ

活用シーン

  1. 01Go 関数のテーブル駆動テストを作成
  2. 02成功ケースとエラーケースを確認
  3. 03環境変数の動作をテスト
  4. 04認証情報が必要なテストをスキップ

インストール

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

$npx skills add https://github.com/tigrisdata/skills --skill go-table-driven-tests

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

同梱ファイルmetadata.jsonREADME.md

詳細

Go のテーブル駆動テスト

Go のテーブル駆動テストを作成または変更する際に、このスキルを使用します。テストが確立されたパターンに従うようにします。

基本原則

  • 1 つのテスト関数で複数のケースをテスト - テストケースをスライスで定義し、t.Run() で反復します
  • 明示的な命名 - 各ケースに name フィールドを設け、サブテスト名として使用します
  • 構造化された入力 - 入力、期待する出力、設定には構造体のフィールドを使用します
  • ヘルパー関数 - テストヘルパーで t.Helper() を使用し、適切な行番号が報告されるようにします
  • 環境のガード - 認証情報が利用できない場合は、統合テストをスキップします

テーブル構造のパターン

func TestFunctionName(t *testing.T) {
    tests := []struct {
        name        string              // required: subtest name
        input       Type                // function input
        want        Type                // expected output
        wantErr     error               // expected error (nil for success)
        errCheck    func(error) bool    // optional: custom error validation
        setupEnv    func() func()       // optional: env setup, returns cleanup
    }{
        {
            name: "descriptive case name",
            input: "test input",
            want: "expected output",
        },
        // ... more cases
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // test implementation using tt fields
        })
    }
}

フィールドのガイドライン

フィールド必須目的
nameはいサブテスト名。内容が具体的に伝わる名前にします
input/args場合によるテスト対象関数への入力値
want/want*場合による期待する出力値(例:wantErr、wantResult)
errCheckいいえカスタムのエラー検証関数
setupEnvいいえクリーンアップ関数を返す環境設定関数

命名規則

  • テスト関数:Test<FunctionName> または Test<FunctionName>_<Scenario>
  • サブテスト名:小文字で、説明的な名前にします。スペースも使用できます
  • 入力フィールド:パラメーター名に合わせるか、input/args を使用します
  • 出力フィールド:want を接頭辞にします(例:want、wantErr、wantResult)

一般的なパターン

1. 基本的なテーブルテスト
func TestWithRegion(t *testing.T) {
    tests := []struct {
        name   string
        region string
    }{
        {"auto region", "auto"},
        {"us-west-2", "us-west-2"},
        {"eu-central-1", "eu-central-1"},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            o := &Options{}
            WithRegion(tt.region)(o)

            if o.Region != tt.region {
                t.Errorf("Region = %v, want %v", o.Region, tt.region)
            }
        })
    }
}
2. wantErr によるエラーチェック
func TestNew_errorCases(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        wantErr error
    }{
        {"empty input", "", ErrInvalidInput},
        {"invalid input", "!!!", ErrInvalidInput},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            _, err := Parse(tt.input)
            if !errors.Is(err, tt.wantErr) {
                t.Errorf("error = %v, want %v", err, tt.wantErr)
            }
        })
    }
}
3. errCheck によるカスタムエラー検証
func TestNew_customErrors(t *testing.T) {
    tests := []struct {
        name     string
        setupEnv func() func()
        wantErr  error
        errCheck func(error) bool
    }{
        {
            name: "no bucket name returns ErrNoBucketName",
            setupEnv: func() func() { return func() {} },
            wantErr:  ErrNoBucketName,
            errCheck: func(err error) bool {
                return errors.Is(err, ErrNoBucketName)
            },
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            cleanup := tt.setupEnv()
            defer cleanup()

            _, err := New(context.Background())

            if tt.wantErr != nil {
                if tt.errCheck != nil {
                    if !tt.errCheck(err) {
                        t.Errorf("error = %v, want %v", err, tt.wantErr)
                    }
                }
            }
        })
    }
}
4. setupEnv による環境設定
func TestNew_envVarOverrides(t *testing.T) {
    tests := []struct {
        name        string
        setupEnv    func() func()
        options     []Option
        wantErr     error
    }{
        {
            name: "bucket from env var",
            setupEnv: func() func() {
                os.Setenv("TIGRIS_STORAGE_BUCKET", "test-bucket")
                return func() { os.Unsetenv("TIGRIS_STORAGE_BUCKET") }
            },
            wantErr: nil,
        },
        {
            name: "bucket from option overrides env var",
            setupEnv: func() func() {
                os.Setenv("TIGRIS_STORAGE_BUCKET", "env-bucket")
                return func() { os.Unsetenv("TIGRIS_STORAGE_BUCKET") }
            },
            options: []Option{
                func(o *Options) { o.BucketName = "option-bucket" },
            },
            wantErr: nil,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            cleanup := tt.setupEnv()
            defer cleanup()

            _, err := New(context.Background(), tt.options...)

            if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
                t.Errorf("error = %v, want %v", err, tt.wantErr)
            }
        })
    }
}

統合テストのガード

実際の認証情報が必要なテストでは、スキップ用ヘルパーを使用します。

// skipIfNoCreds skips the test if Tigris credentials are not set.
// Use this for integration tests that require real Tigris operations.
func skipIfNoCreds(t *testing.T) {
    t.Helper()
    if os.Getenv("TIGRIS_STORAGE_ACCESS_KEY_ID") == "" ||
        os.Getenv("TIGRIS_STORAGE_SECRET_ACCESS_KEY") == "" {
        t.Skip("skipping: TIGRIS_STORAGE_ACCESS_KEY_ID and TIGRIS_STORAGE_SECRET_ACCESS_KEY not set")
    }
}

func TestCreateBucket(t *testing.T) {
    tests := []struct {
        name    string
        bucket  string
        options []BucketOption
        wantErr error
    }{
        {
            name:    "create snapshot-enabled bucket",
            bucket:  "test-bucket",
            options: []BucketOption{WithEnableSnapshot()},
        },
    }

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

            // test implementation
        })
    }
}

テストヘルパー

適切な行番号が報告されるよう、ヘルパー関数で t.Helper() を使用します。

func setupTestBucket(t *testing.T, ctx context.Context, client *Client) string {
    t.Helper()
    skipIfNoCreds(t)

    bucket := "test-bucket-" + randomSuffix()
    err := client.CreateBucket(ctx, bucket)
    if err != nil {
        t.Fatalf("failed to create test bucket: %v", err)
    }
    return bucket
}

func cleanupTestBucket(t *testing.T, ctx context.Context, client *Client, bucket string) {
    t.Helper()
    err := client.DeleteBucket(ctx, bucket, WithForceDelete())
    if err != nil {
        t.Logf("warning: failed to cleanup test bucket %s: %v", bucket, err)
    }
}

チェックリスト

テーブル駆動テストを作成する際:

  • テーブル構造体の最初のフィールドが name である
  • 各テストケースに内容が伝わる名前が付いている
  • 入力フィールドに分かりやすい名前を使用している(パラメーター名に合わせるか、input を使用)
  • 期待する出力フィールドに want を接頭辞として付けている
  • t.Run(tt.name, func(t *testing.T) { ... }) を使って反復している
  • エラーチェックでエラーを比較する際に errors.Is() を使用している
  • 環境設定で defer を使ってクリーンアップしている
  • 統合テストで skipIfNoCreds(t) ヘルパーを使用している
  • テストヘルパーで適切な行番号が報告されるよう t.Helper() を使用している
  • テストファイルが *_test.go で、テスト対象のコードと同じ場所にある

ベストプラクティス

詳細なエラーメッセージ

失敗の原因を明確に特定できるよう、エラーメッセージに実際の値と期待する値の両方を含めます。

t.Errorf("got %q, want %q", actual, expected)

注: t.Errorf はアサーションではありません。ログを記録した後もテストは続行されます。これにより、失敗が全体的に発生しているのか、特定のケースに限られているのかを特定しやすくなります。

テストケースにマップを使用する

テストケースにスライスではなくマップを使用することを検討してください。マップの反復順序は非決定的なため、テストケースが本当に独立していることを確認できます。

tests := map[string]struct {
    input string
    want  string
}{
    "empty string":       {input: "", want: ""},
    "single character":   {input: "x", want: "x"},
    "multi-byte glyph":   {input: "🎉", want: "🎉"},
}

for name, tt := range tests {
    t.Run(name, func(t *testing.T) {
        got := process(tt.input)
        if got != tt.want {
            t.Errorf("got %q, want %q", got, tt.want)
        }
    })
}
並列テスト

テストケースを並列実行するには、t.Parallel() 呼び出しを追加します。ループ変数は各反復で自動的にキャプチャされます。

func TestFunction(t *testing.T) {
    tests := []struct {
        name string
        input string
    }{
        // ... test cases
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // marks this subtest as parallel
            // test implementation
        })
    }
}

参考資料

コメント