Go開発者必見 sqlmock.AnyArg()の落とし穴とその回避法
はじめに
個人開発でsqlmockを使用しています。タイムスタンプ型のカラムに対してsqlmock.AnyArg()を設定した際にsqlmock.AnyArg()が機能しない事象が発生しました。
今回はその問題の内容と対応方法をまとめてみました。
sqlmockとは
実際のDBの様な動きをするモックライブラリーです。こちらを使うことでローカル環境などで実際のDBを使用せずとも開発やUTを行うことができます。テスト対象のコードが期待した通りにトランクザクション開始したのか、コミットやロールバックを実行したのか、期待したSQLを実行したかを検証することも可能です。
sqlmock.AnyArg()とは
前述した通り、sqlmockはSQLの検証をします。この時、SQLに組み込まれるパラメータの値も対象となります。
パラメータの期待値にsqlmock.AnyArg()を設定することで、そのパラメータはあらゆる値とマッチする設定になります。
発生した問題
下記のテストコードが今回、問題が発生したコードです。
// sqlmockでコネクション取得
dbm, mock, err := sqlmock.New()
if err != nil {
log.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
dialector := postgres.New(postgres.Config{
DSN:"sqlmock_db_0",
DriverName:"postgres",
Conn:dbm,
PreferSimpleProtocol: true,
})
gormDB, err := gorm.Open(dialector, &gorm.Config{})
if err != nil {
log.Fatalf("an error '%s' was not expected when initializing a gorm db connection", err)
}
// トランザクション開始するか検証
mock.ExpectBegin()
// projectsテーブルにname:testproject1、user_id:1、created_atとupdated_atは任意の値でINSERTするか検証
mock.ExpectExec("INSERT INTO \"projects\" (\"name\",\"user_id\" ,\"created_at\",\"updated_at\") VALUES ($1,$2,$3,$4) RETURNING \"id\"").
WithArgs("testproject1", 1, sqlmock.AnyArg(), sqlmock.AnyArg())
// project_user_mappingsテーブルにname:任意の値、user_id:1でINSERTするか検証
mock.ExpectExec("INSERT INTO \"project_user_mappings\"(\"project_id\",\"user_id\") VALUES ($1,$2) RETURNING \"id\"").
WithArgs(sqlmock.AnyArg(), 1)
// コミットするか検証
mock.ExpectCommit()
実施にテストコードを実行すると以下のエラーメッセージが出力されます。
35;1mcall to Query 'INSERT INTO "projects" ("name","user_id","created_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id"' with args [{Name: Ordinal:1 Value:testproject1} {Name: Ordinal:2 Value:1} {Name: Ordinal:3 Value:2024-08-24 19:16:16.770734 +0900 JST} {Name: Ordinal:4 Value:2024-08-24 19:16:16.770734 +0900 JST}], was not expected, next expectation is: ExpectedExec => expecting Exec or ExecContext which:
- matches sql: 'INSERT INTO "projects" ("name","user_id" ,"created_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id"'
- is with arguments:
0 - testproject1
1 - 1
2 - {}
3 - {}
こちらのエラーメッセージは実際に実行したSQLと期待したSQLが異なるとの内容です。
比較してみますと、nameとuser_idは引数の値が一致しています。
しかし、created_atとupdated_atはsqlmock.AnyArg()の部分が {} と表示されており、実際の引数には 2024-08-24 19:16:16.770734 +0900 JST という具体的な値が渡されています。
本来ならsqlmock.AnyArg()が設定されているため、任意の値とマッチすることになっているのですが、なぜか不一致となりエラーとなってしまいます。
何らかの原因でテストコードの以下の箇所で定義したsqlmock.AnyArg()が機能していない様に見受けられます。
// projectsテーブルにname:testproject1、user_id:1、created_atとupdated_atは任意の値でINSERTするか検証
mock.ExpectExec("INSERT INTO \"projects\" (\"name\",\"user_id\" ,\"created_at\",\"updated_at\") VALUES ($1,$2,$3,$4) RETURNING \"id\"").
WithArgs("testproject1", 1, sqlmock.AnyArg(), sqlmock.AnyArg())
解決方法
sqlmock.AnyArg()の設定はそのままにし、「ExpectExec」を「ExpectQuery」に変更し、SQLを「regexp.QuoteMeta」で囲む様に修正しました。
// sqlmockでコネクション取得
dbm, mock, err := sqlmock.New()
if err != nil {
log.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
dialector := postgres.New(postgres.Config{
DSN:"sqlmock_db_0",
DriverName:"postgres",
Conn:dbm,
PreferSimpleProtocol: true,
})
gormDB, err := gorm.Open(dialector, &gorm.Config{})
if err != nil {
log.Fatalf("an error '%s' was not expected when initializing a gorm db connection", err)
}
// トランザクション開始するか検証
mock.ExpectBegin()
// projectsテーブルにname:testproject1、user_id:1、created_atとupdated_atは任意の値でINSERTするか検証
resp := sqlmock.NewRows([]string{"id"}).AddRow(1)
mock.ExpectQuery(regexp.QuoteMeta(
`INSERT INTO "projects" ("name","user_id","created_at","updated_at") VALUES ($1,$2,$3,$4)`)).
WithArgs(test1ProjectDto.Name, test1ProjectDto.UserID, sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnRows(resp)
// project_user_mappingsテーブルにname:任意の値、user_id:1でINSERTするか検証
respm := sqlmock.NewRows([]string{"id"}).AddRow(1)
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "project_user_mappings" ("project_id","user_id") VALUES ($1,$2)`)).
WithArgs(1, test1ProjectDto.UserID).
WillReturnRows(respm)
// コミットするか検証
mock.ExpectCommit()
実行すると、テストコードが正常に実行されたことが確認できました。
Running tool: /opt/homebrew/bin/go test -timeout 30s -run ^Test_projectRepository_CreateProject$ task-go/repository
ok task-go/repository
まとめ
タイムスタンプ型のカラムにsqlmock.AnyArg()を設定すると機能しないケースがあり、「ExpectQuery」を使用し、SQLを「regexp.QuoteMeta」で囲む様にすると機能する様になります。
Discussion