go-table-driven-tests について
Go のテーブル駆動テストを作成・変更する開発者向けのスキルです。テストケースやサブテスト、ヘルパーのパターンに加え、環境の後処理や認証情報がない場合の統合テストのスキップも扱います。
主な機能
- 01テーブルでケースを整理
- 02t.Run() で名前付きケースを実行
- 03期待値とエラーを検証
- 04テスト環境を設定・後処理
- 05認証情報がなければ統合テストをスキップ
活用シーン
- 01Go 関数のテーブル駆動テストを作成
- 02成功ケースとエラーケースを確認
- 03環境変数の動作をテスト
- 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
})
}
}
参考資料
- Go Wiki:TableDrivenTests - テーブル駆動テストに関する Go コミュニティの公式ベストプラクティス
- Go Testing Package - 標準ライブラリのテストに関するドキュメント
- テーブル駆動テストを優先する - 従来のテスト構造よりテーブル駆動テストをいつ、なぜ使用するかについての Dave Cheney によるガイド
コメント
Markdown 対応