Go言語において、table-driven test(テーブル駆動テスト)は一般的で推奨されているテスト手法です。
このスタイルは「データとロジックの分離」を重視しており、テストコードをより簡潔かつ保守しやすくします。
特に複数の入力/出力のテストケースを扱う場面に適しています。
なぜ Table-Driven Test を使うのか?
同じ関数に対して複数のケースをテストする場合、
各ケースを手動で t.Run(...)
で書くと、コードが冗長になり、保守が困難になります。
table-driven テストを使うことで、以下のような利点があります:
- テストケースを集中して管理できる
- ループで自動的にテストを実行し、重複コードを回避できる
- 可読性と一貫性が向上する
起源と提唱者
table-driven test は特定の開発者が考案したデザインパターンではなく、Go のコア開発者である Andrew Gerrand によって、講演やブログなどで広く推奨された実践的な手法です。
「Goでは、私たちはしばしばテーブル駆動のテストを使用します。これは、コードを重複させずに複数のシナリオをテストする良い方法です。」
— Andrew Gerrand, Go 開発者アドボケイト
このスタイルは、strings
, strconv
, time
などの Go 標準ライブラリのテストファイルにも見られ、idiomatic Go(慣用的なGo)として定着しています。
実装例:通知タイプの有効性チェック
以下は、入力された通知タイプが "email"、"push"、"sms" のいずれかであるかを検証するシンプルな例です。
テスト対象コード(notification.go)
package notification
func IsValidNotificationType(t string) bool {
switch t {
case "email", "push", "sms":
return true
default:
return false
}
}
テストコード(notification_test.go)
package notification
import "testing"
func TestIsValidNotificationType(t *testing.T) {
tests := []struct {
name string
input string
expected bool
}{
{"valid_email", "email", true},
{"valid_push", "push", true},
{"valid_sms", "sms", true},
{"invalid_type", "fax", false},
{"empty_string", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := IsValidNotificationType(tt.input)
if actual != tt.expected {
t.Errorf("IsValidNotificationType(%q) = %v; want %v", tt.input, actual, tt.expected)
}
})
}
}
解説のポイント
-
tests := []struct{...}
:複数のテストデータを定義 -
t.Run(tt.name, ...)
:各テストケースを個別に実行し、名前で識別(デバッグしやすくなる) - テストが失敗した場合、
t.Errorf
によって詳細な情報が出力される
応用例
table-driven テストは以下のような場面でも有効です:
- 境界値テストなど、複数の入力条件の組み合わせ
- 複数のサブ関数に共通するロジックの検証
- ユニットテストや統合テストにおけるデータの組み合わせ処理
まとめ
Go における table-driven テストスタイルはシンプルで読みやすく、保守もしやすいため、ユニットテストを書く際のベストプラクティスの一つです。複数の条件をカバーする必要がある場合、このスタイルをぜひ検討してください。
参考文献
- Go Wiki - "Table-Driven Tests"
- Andrew Gerrand, "Go testing techniques"