テーブル駆動テスト - t.Runと構造設計の評価基準
テーブル駆動テスト完全レビューガイド:t.Runと構造設計の評価基準
Go言語ではテーブル駆動テスト(Table-driven test)が非常に定着しているが、
レビュー現場では「書き方は似ていても、設計としての良し悪しが大きく分かれる」という状況が頻発する。
レビューアーの役割は「動くかどうか」ではなく、保守性・診断性・構造性を評価することにある。
良い実装例:典型的なテーブル駆動テスト構造
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"1+1=2", 1, 1, 2},
{"2+3=5", 2, 3, 5},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}
- nameが具体的で失敗時の原因特定が容易
- tt := ttによるクロージャ安全化
- 入力・期待値の対応が明快
このように「読むだけで意図がわかる設計」になっているかがまず良い設計の判断軸。
問題のある実装例とレビュー指摘
① テーブル名が曖昧で診断性が低い
tests := []struct {
name string
a, b int
want int
}{
{"case1", 1, 1, 2},
{"case2", 2, 3, 5},
}
@Reviewernameに具体性がありません。失敗時に何のケースか特定できるよう、計算式や条件内容を名称に反映してください。
CIログに「case2 failed」とだけ出ても、後続調査に無駄な時間が発生する。
レビューでは name の設計意図明示性を最重要確認ポイントに置くべき。
② クロージャキャプチャ問題(tt := tt忘れ)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
@Reviewerループ変数ttをクロージャ内で直接参照しています。必ずtt := ttと代入コピーし、各サブテストが正しい値を参照するよう修正してください。
Goではループ変数は毎回上書き更新される。
サブテストの並列実行時に全て最後の値を参照するバグが多発する要注意パターン。
③ テーブル詰め込み過ぎで可読性崩壊
tests := []struct {
a int
b int
op string
want int
}{
{1, 2, "+", 3},
{2, 2, "*", 4},
}
@Reviewerテーブルに処理分岐条件(op)が含まれており、テストロジック側が煩雑化しています。各演算ごとに関数・テストを分割してください。
テーブルに「操作種別」「期待値」「分岐条件」を詰め込みすぎると、
レビューが容易な「表形式」から「ミニ実装ロジック」に変質してしまう。
④ t.Run入れ子深度の過剰化
func TestService(t *testing.T) {
t.Run("正常系", func(t *testing.T) {
t.Run("サブケースA", func(t *testing.T) {
t.Run("さらに詳細", func(t *testing.T) {
// ...
})
})
})
}
@Reviewert.Runのネストが深すぎます。最大2階層程度に留め、全体の視認性を確保してください。詳細分岐はテーブル分割で整理しましょう。
t.Runは深すぎるとCIログ読解コストが跳ね上がる。
レビューでは「適切な粒度分割」を誘導する指摘が有効。
⑤ assert過剰活用で診断性低下
assert.Equal(t, 42, got, "unexpected value")
assert.NoError(t, err)
assert.True(t, cond)
@Reviewerassertを羅列しすぎています。失敗時にどの条件で落ちたのか特定困難です。条件ごとに事前にローカル変数整理し、エラーメッセージに具体性を持たせてください。
簡潔さと診断性のバランスがレビュー評価軸になる。
assertは便利だが安易な一行化で事故率が跳ね上がる。
テーブル駆動テスト構造の図解
実務レビューで見抜きたい構造設計ポイント
テストケース自体の可読性
- 表形式が保たれているか
- 期待値の把握が即可能か
サブテスト分割方針
- 上位区分(正常系/異常系/境界値系)で分割整理
- 深さは原則2階層まで
フィールド設計
- 本質的な入力と出力に限定
- 演算分岐・異常分岐は別テーブルに整理
クロージャ安全性(ループ変数)
- tt := tt は必須ルール
テストデータ量制御
- 10ケース超過ならテーブル分割を検討
テストレビュー観点チェックリスト
チェック項目 | 内容 |
---|---|
nameの明確性 | 失敗時にCIログで何のケースか特定可能か |
変数キャプチャ安全化 | tt := tt を忘れていないか |
フィールド肥大化抑止 | 入力・期待値以外の情報混在がないか |
t.Run入れ子抑制 | ネスト2階層以下に収められているか |
assert診断性 | assert羅列で落下地点不明になっていないか |
ケース数の妥当性 | 必要十分なケース数に整理されているか |
さらにレビューすべき実務事故事例
① テーブル肥大 → 別責務混在事故
tests := []struct {
inputA string
inputB int
mockExternal bool
expectDBInsert bool
wantErr bool
}{...}
@Reviewer外部依存やDB挿入判定まで同一テーブルに混在しています。I/O境界はテスト単位を分割し責務を整理してください。
② 失敗出力が無情報化
t.Errorf("unexpected result")
@Reviewer期待値と実測値をログに出力してください。例えば: got=%v, want=%v の形式で出力を整備しましょう。
レビューで真っ先に確認すべきは失敗時に十分なデバッグ情報が出るか。
テスト設計レビューは「将来の変更耐性」を読む作業
テーブル駆動テストのレビューでは「今のコード」ではなく
「将来新ケースを追加した時に破綻しないか」 を評価する目線が重要になる。
- 表設計に余白があるか?
- テスト関数の責務が肥大化していないか?
- t.Run入れ子がメンテ時に破綻しないか?