go-test-table-driven について
Go 開発者やコーディングエージェント向けのテストスキルです。テーブル駆動テストの適用場面と、ケースやループを簡潔に保つ方法を説明し、並列実行や複雑なテーブルのリファクタリングも扱います。
主な機能
- 01テーブル駆動テストの適用判断
- 02テストケースを簡潔に保つ
- 03説明的なケース名の使用
- 04ループ処理の簡素化
- 05安全なケースの並列実行
- 06肥大化したテーブルのリファクタリング
活用シーン
- 01共通パターンでパーサーやバリデーターをテスト
- 02テーブルと明示的なサブテストを選択
- 03分岐や複雑な準備を含むテーブルを整理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-test-table-drivenインストール後、Claude Code で/go-test-table-drivenを入力して読み込まれたか確認します。
同梱ファイルreferences/patterns.mdreferences/refactoring.md
詳細
Goのテーブル駆動テスト
テーブル駆動テストは、正しく使えばGoの強力なイディオムです。多くのコードベースでは、活用が不十分(テストを10個コピー&ペースト)か、過剰に活用されています(200行の構造体に複雑な分岐ロジックを詰め込む)。このスキルでは、ちょうどよい使い方を説明します。
コミュニティのデフォルトです。
go-test-table-drivenを明示的に上書きするプロジェクトのスキルまたはエージェント設定ルールがある場合は、そちらが優先されます。
必要に応じて読み込む詳細なリファレンス資料:
references/patterns.md— 完全な実例:標準的なテーブル、wantErr/wantErrIs、並列テーブル、マップベースのテーブル、エラーのみのテーブル、読みやすさを考慮した構造体の配置。references/refactoring.md— 肥大化したテーブルの見分け方と、明示的なサブテストへの書き換え方。変更前後の例付きです。
以下の概要だけでは現在のタスクに不十分な場合に限り、リファレンスファイルを読んでください。
1. テーブル駆動テストが適している場合
次の条件がすべて当てはまる場合にのみ、テーブルを使用します。
- すべてのケースで同じ関数をテストする
- アサーションのパターンが同じ — 入力を渡し、出力を比較する
- ケース間で異なるのはデータだけであり、セットアップや検証ロジックではない
- ケースが3つ以上 — 3つ未満なら、明示的なテストのほうが明確です
標準的な用途:純粋関数、パーサー、バリデーター、フォーマッター。
func TestParseSize(t *testing.T) {
tests := []struct {
name string
input string
want int64
wantErr bool
}{
{name: "plain bytes", input: "1024", want: 1024},
{name: "kilobytes suffix", input: "4KB", want: 4096},
{name: "empty string", input: "", wantErr: true},
{name: "negative size", input: "-1", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseSize(tt.input)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
すべてのケースが同じ形をしており、ループ本体は数行で済み、ケースの追加は構造体リテラルを1つ追加するだけです。分岐も条件文もありません。
2. テーブル駆動テストを使わない場合
- ケースごとのセットアップが複雑 — 構造体に
setupMock/setupFunc関数フィールドがある場合、テーブルが複雑さを隠しています。明示的なサブテストを書いてください。 - ケースが3つ未満 — 構造体の定義のほうが、通常のテスト関数を2つ書くよりコード量が多くなります。
- 複数の分岐経路がある — ループ本体の
if tt.shouldError/if tt.wantRedirectは、それぞれの分岐が、同じ構造を共有しているかのように見せかけた別々のテストであることを意味します。分割してください。
それぞれの問題を修正する前後の例については、references/refactoring.md を参照してください。
3. 構造体の設計ルール
- 各フィールドは、少なくとも2つのケース間で値が異なる必要があります。 どのケースでも同じ値のフィールドはセットアップ用です。テーブルの外に移してください。
nameフィールドは、シナリオを説明する短い文にします。"case1"や"success"ではなく、"returns error for negative amount"のようにします。- 「エラーになるべきか」の判定には
wantErr boolを使います。 — 最初に確認し、ループ本体では早期にreturnします。 - 呼び出し元が特定のエラーを判別する必要がある場合は、センチネルエラーと
wantErrIs errorを使います。require.ErrorIsでアサートします。 - フィールドは5つ以下にします。 それ以上ある場合、シナリオがテーブルに適さないほど複雑です。別々のテスト関数に分割してください。
フィールドパターンの詳細な例は、references/patterns.md にあります。
4. ループ本体は単純にする
テーブルテストの目的は、すべてのケースで同じ実行ロジックを使うことです。ループ本体は約10行以内に収めます。関数呼び出し、エラーチェック、比較だけにしてください。条件文やケースごとのセットアップが増えてきたら、テーブルの有用性が失われています。明示的なサブテストにリファクタリングしてください。
5. 並列テーブルテスト
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := Transform(tt.input)
assert.Equal(t, tt.want, got)
})
}
- Go 1.22以降では、ループ変数は反復ごとにスコープが設定されるため、
tt := ttによるキャプチャは不要です。Go 1.22未満では、引き続きキャプチャが必要です。 - テスト対象の関数に副作用がなく、共有の可変状態もない場合に限り、
t.Parallel()を使用してください。
6. 肥大化したテーブルのリファクタリング
| 症状 | 修正 |
|---|---|
| 構造体にフィールドが8つ以上ある | シナリオごとに複数のテスト関数に分割する |
構造体に setupFunc フィールドがある | 明示的なセットアップを行う別々のサブテストに切り出す |
ループ本体に if tt.shouldX がある | それぞれの分岐は別のテストです。分割してください |
| すべてのケースで同じフィールドが3つある | テーブルの外にある共通のセットアップに移す |
| ケースを追加するために他のすべてのケースを理解する必要がある | テーブルの有用な寿命を超えて肥大化しています |
判断フローチャート
-
関数は純粋ですか(入力 → 出力、副作用なし)? はい → テーブルテストが最適でしょう。2に進みます。 いいえ → まず明示的なサブテストを検討します。
-
すべてのケースで、まったく同じアサーションパターンを共有していますか? はい → テーブルテストです。3に進みます。 いいえ → 明示的なサブテストです。
-
各ケースを5個以下の構造体フィールドで表現できますか? はい → テーブルテストです。 いいえ → シナリオごとに別のテスト関数に分けます。
-
ループ本体は10行以下ですか? はい → 申し分ありません。 いいえ → テーブルに複雑さが隠れています。リファクタリングしてください。
検証チェックリスト
- テーブル構造体には、ケースごとに異なるフィールドだけがある
- すべてのケースに説明的な
nameフィールドがある - ループ本体は分岐なしで10行以下である
- 構造体に
setupFuncフィールドやmockFuncフィールドがない wantErrは単純な bool またはセンチネルであり、文字列との照合ではない- ケースには正常系、エラー系、エッジケース(空、nil、ゼロ、最大値)が含まれる
- 名前付きサブテストにするため、各ケースを
t.Runで囲む t.Parallel()は関数に副作用がない場合のみ使用する