🐦

Go開発者必見 sqlmock.AnyArg()の落とし穴とその回避法

2024/08/25に公開

はじめに

個人開発で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