こんにちは!フリーランスエンジニアのこたろうです。
データベースを使用したアプリケーションのテストについて、学びで得た知見を共有します。
なぜsqlmockが必要なのか?
データベースを使用するアプリケーションのテストには、以下のような課題があります:
- テスト用のデータベース環境が必要
- テストの実行に時間がかかる
- テストデータの準備が大変
- 特定の状況(エラーケースなど)のテストが難しい
これらの課題を解決するのが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
}
このセットアップ関数が行っていることは:
- sqlmockでモックのデータベース接続を作成
- そのモック接続をGORMで使えるように変換
- テストで使用する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()) // モックの期待値通りに実行されたこと
}
このテストが確認していることは:
- 正しいSQL文が実行されるか
- SQLのパラメータが正しいか
- エラーが発生しないか
- モックの期待値通りに処理が行われたか
よくある課題と解決策
1. 時刻フィールドの扱い
GORMはcreated_at
やupdated_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(), "データベースエラー")
}
まとめ:効果的なテストを書くためのポイント
-
SQL文の正確な把握
- 実際に実行されるSQLを確認
- 正規表現のパターンを正確に
- プレースホルダの順序に注意
-
時刻データの取り扱い
-
time.Now()
はUTC()
とTruncate()
を使用 - 必要に応じて
sqlmock.AnyArg()
を活用
-
-
トランザクション管理
- Begin/Commitの順序を正しく
- ロールバックのテストも忘れずに
-
エラーケースのテスト
- データベースエラーの場合の動作確認
- バリデーションエラーの確認
- エッジケースの考慮
sqlmockを使いこなすことで、データベース操作を含むコードでも、安定した高品質なテストが書けるようになります。