0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goテスト駆動開発 ビルダーパターンファクトリメソッド使い分け dbに接続するテストをするときのテストデータの作り方

Posted at

この記事でわかること

  • 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」が扱いやすい
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?