0
0

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/GORM】sqlmockを使ったユニットテストの実践的な書き方

Posted at

こんにちは!フリーランスエンジニアのこたろうです。
データベースを使用したアプリケーションのテストについて、学びで得た知見を共有します。

なぜsqlmockが必要なのか?

データベースを使用するアプリケーションのテストには、以下のような課題があります:

  1. テスト用のデータベース環境が必要
  2. テストの実行に時間がかかる
  3. テストデータの準備が大変
  4. 特定の状況(エラーケースなど)のテストが難しい

これらの課題を解決するのがsqlmockです。sqlmockを使うと:

  • 実際のデータベース不要
  • テストの実行が高速
  • あらゆるケースのテストが可能
    になります。

sqlmockの基本的な使い方

セットアップ方法

まず、テスト用のデータベース接続をモック化します:

func setupMockDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock) {
    // モックのDB接続を作成
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Fatalf("sqlmockの作成に失敗: %v", err)
    }

    // GORMでモックDBを扱えるようにする
    gormDB, err := gorm.Open(postgres.New(postgres.Config{
        Conn: db,
    }), &gorm.Config{})
    if err != nil {
        t.Fatalf("GORMの初期化に失敗: %v", err)
    }

    return gormDB, mock
}

このセットアップ関数が行っていることは:

  1. sqlmockでモックのデータベース接続を作成
  2. そのモック接続をGORMで使えるように変換
  3. テストで使用するDB接続とモックを返す

基本的なテストの書き方

タスク作成のテストを例に見てみましょう:

func TestCreateTask(t *testing.T) {
    // 1. セットアップ
    db, mock := setupMockDB(t)
    repo := NewTaskRepository(db)

    // 2. テストデータの準備
    task := &Task{
        Title:       "テストタスク",
        Description: "詳細説明",
    }

    // 3. モックの期待値を設定
    // INSERT文が実行されることを期待
    mock.ExpectExec(`INSERT INTO "tasks" \("title","description"\) VALUES \(\$1,\$2\) RETURNING "id"`).
        // プレースホルダの値を指定
        WithArgs(task.Title, task.Description).
        // 戻り値を設定(ID=1, 影響行数=1)
        WillReturnResult(sqlmock.NewResult(1, 1))

    // 4. テスト対象の関数を実行
    err := repo.Create(task)
    
    // 5. 検証
    assert.NoError(t, err)                          // エラーが発生していないこと
    assert.NoError(t, mock.ExpectationsWereMet())   // モックの期待値通りに実行されたこと
}

このテストが確認していることは:

  1. 正しいSQL文が実行されるか
  2. SQLのパラメータが正しいか
  3. エラーが発生しないか
  4. モックの期待値通りに処理が行われたか

よくある課題と解決策

1. 時刻フィールドの扱い

GORMはcreated_atupdated_atを自動で設定します。これらの時刻は常に変化するため、テストが不安定になりがちです。

// 問題のあるコード
mock.ExpectExec(`UPDATE "tasks"`).
    WithArgs(task.Title, time.Now()).  // ← これは失敗しやすい
    WillReturnResult(sqlmock.NewResult(1, 1))

// 改善コード
mock.ExpectExec(`UPDATE "tasks"`).
    WithArgs(task.Title, sqlmock.AnyArg()).  // どんな時刻でもOK
    WillReturnResult(sqlmock.NewResult(1, 1))

// さらに良いコード
now := time.Now().UTC().Truncate(time.Microsecond)
task.UpdatedAt = now
mock.ExpectExec(`UPDATE "tasks"`).
    WithArgs(task.Title, now).  // 具体的な時刻を指定
    WillReturnResult(sqlmock.NewResult(1, 1))

2. トランザクションのテスト

トランザクションを使用する場合、Begin/Commit(またはRollback)の期待値も設定する必要があります:

func TestCreateTaskWithTransaction(t *testing.T) {
    db, mock := setupMockDB(t)
    repo := NewTaskRepository(db)

    task := &Task{Title: "トランザクションテスト"}

    // トランザクションの流れを定義
    mock.ExpectBegin()  // トランザクション開始
    mock.ExpectExec(`INSERT INTO "tasks"`).
        WithArgs(task.Title, sqlmock.AnyArg(), sqlmock.AnyArg()).
        WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectCommit() // トランザクション終了

    err := repo.CreateWithTransaction(task)
    assert.NoError(t, err)
}

3. エラー処理のテスト

データベースエラーの場合のテストも書けます:

func TestCreateTask_Error(t *testing.T) {
    db, mock := setupMockDB(t)
    repo := NewTaskRepository(db)

    task := &Task{Title: "エラーテスト"}

    // エラーを返すように設定
    mock.ExpectExec(`INSERT INTO "tasks"`).
        WithArgs(task.Title).
        WillReturnError(fmt.Errorf("データベースエラー"))

    err := repo.Create(task)
    assert.Error(t, err) // エラーが返されることを確認
    assert.Contains(t, err.Error(), "データベースエラー")
}

まとめ:効果的なテストを書くためのポイント

  1. SQL文の正確な把握

    • 実際に実行されるSQLを確認
    • 正規表現のパターンを正確に
    • プレースホルダの順序に注意
  2. 時刻データの取り扱い

    • time.Now()UTC()Truncate()を使用
    • 必要に応じてsqlmock.AnyArg()を活用
  3. トランザクション管理

    • Begin/Commitの順序を正しく
    • ロールバックのテストも忘れずに
  4. エラーケースのテスト

    • データベースエラーの場合の動作確認
    • バリデーションエラーの確認
    • エッジケースの考慮

sqlmockを使いこなすことで、データベース操作を含むコードでも、安定した高品質なテストが書けるようになります。

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?