この記事でわかること
- Goのテスト駆動開発で、テストデータ生成をどう設計するか
- ビルダーパターンとファクトリメソッドの境界線と使い分け
- DB接続テストで安全・高速・再現性のあるテストデータの作り方
1. 前提:テストデータ生成は「仕様の表現」
テストは仕様を表現するものなので、テストデータの作り方はテストの読みやすさと保守性を大きく左右します。
ここで重要なのは:
- 関心の分離(テストの意図 vs 生成の詳細)
- 可読性(テストのシナリオが読める)
- 再利用性(別テストで共通のデータを使える)
このため、生成パターンを使い分けます。
2. ビルダーパターン vs ファクトリメソッド
2.1 ファクトリメソッド
- 目的:単一の分かりやすい生成
- 利点:呼び出しがシンプル
- 欠点:派生ケースが増えると関数が増殖
// User はドメインの例
// ファクトリメソッド例(テスト専用として *_test.go に配置)
func NewActiveUser() User {
return User{
ID: "u-001",
Name: "Alice",
Email: "alice@example.com",
Active: true,
}
}
適用場面
- 1〜2種類の固定データ
- 「これで十分」な標準ケース
2.2 ビルダーパターン
- 目的:柔軟な生成とテスト意図の明示
- 利点:テストコードで必要な差分だけ記述できる
- 欠点:ビルダー自体の実装コスト
// ビルダーパターン例(テスト専用として *_test.go に配置)
// 変更点だけ指定できる
type UserBuilder struct {
user User
}
func NewUserBuilder() *UserBuilder {
return &UserBuilder{user: User{
ID: "u-default",
Name: "Default",
Email: "default@example.com",
Active: true,
}}
}
func (b *UserBuilder) WithID(id string) *UserBuilder {
b.user.ID = id
return b
}
func (b *UserBuilder) WithName(name string) *UserBuilder {
b.user.Name = name
return b
}
func (b *UserBuilder) WithInactive() *UserBuilder {
b.user.Active = false
return b
}
func (b *UserBuilder) Build() User {
return b.user
}
テスト側
func TestUserBuilder_Inactive(t *testing.T) {
t.Run("ユーザービルダー_非アクティブ生成", func(t *testing.T) {
u := NewUserBuilder().WithID("u-999").WithInactive().Build()
if u.Active {
t.Fatalf("expected inactive user")
}
})
}
適用場面
- パラメータが多い
- テストケースのバリエーションが多い
- 「この値だけ変えたい」が頻繁に起きる
3. 実務での使い分け指針
| シーン | 推奨 | 理由 |
|---|---|---|
| 固定の標準データ | ファクトリメソッド | シンプルで分かりやすい |
| テストケースが多い | ビルダー | 変更点を明示できる |
| オブジェクトが重い | ビルダー + ファクトリ | まず標準生成し部分差分を書く |
4. DB接続テストでのデータ作成
DBに接続する統合テストでは、テストデータの独立性と速度が重要です。ここでは一般的な構成を例にします。
4.1 トランザクションを使った「使い捨て」データ
- 各テストでトランザクション開始
- テスト終了時に
Rollback() - DB状態を汚さない
func withTx(t *testing.T, db *sql.DB, fn func(tx *sql.Tx)) {
t.Helper()
tx, err := db.Begin()
if err != nil {
t.Fatalf("begin tx: %v", err)
}
defer tx.Rollback()
fn(tx)
}
4.2 テストデータの作成は「Builder + Inserter」
- Builder:テストで表現したい値
- Inserter:DBに保存する責務
注記: テストデータのDBインサートは、GORM などの ORM を使って統一しても問題ありません。
この記事では仕組みが見えやすいように素のSQLで記述しています。
// テストデータInserter
func InsertUser(t *testing.T, tx *sql.Tx, u User) {
t.Helper()
_, err := tx.Exec(`INSERT INTO users(id, name, email, active) VALUES (?, ?, ?, ?)`,
u.ID, u.Name, u.Email, u.Active,
)
if err != nil {
t.Fatalf("insert user: %v", err)
}
}
4.3 テストコード例
func TestFindActiveUsers_WithTransaction(t *testing.T) {
t.Run("DB接続_アクティブユーザー取得_トランザクションで独立", func(t *testing.T) {
db := openTestDB(t)
withTx(t, db, func(tx *sql.Tx) {
// テストデータ準備
activeUser := NewUserBuilder().WithID("u-1").Build()
inactiveUser := NewUserBuilder().WithID("u-2").WithInactive().Build()
InsertUser(t, tx, activeUser)
InsertUser(t, tx, inactiveUser)
// SUT
got, err := FindActiveUsers(tx)
if err != nil {
t.Fatalf("find active users: %v", err)
}
if len(got) != 1 || got[0].ID != "u-1" {
t.Fatalf("unexpected result: %+v", got)
}
})
})
}
5. 実務Tips
- テストデータ作成は一箇所に集約(builder / inserter)
- テストにしか使わないfixtureを作り過ぎない(共通化し過ぎると読めなくなる)
- テストコードは愚鈍さも必要(意図が薄れる共通化は避け、必要なら実装を素直にコピー)
- 日付や性別など非決定的なデータは使わない(ランダム生成もしない)
- DBの初期化コストが大きい場合は testcontainers-go を検討
注記: フィールド変数の値を直接確認するテストは非常に壊れやすく、原則として導入しない方が良いと考えています。
ただし本サンプルでは動作確認のために、あえてフィールド確認テストを含めています。
まとめ
- ファクトリメソッドは「固定パターン生成」
- ビルダーパターンは「テスト意図を明示しつつ差分生成」
- DB接続テストでは「トランザクション + Builder + Inserter」が扱いやすい