概要 と 解説
sqlmock はデータベースをモック化して、テストするのに便利なライブラリです。
sqlmock に AnyString{}
という Struct を定義することで、任意の文字列を Args に与えられるようにします。
たとえば、 user_id を持つ users テーブルへの検索をする際に、次のようなコードで実際のデータベースを用意しなくてもテストすることができます。
次のコードはその疑似コードです。
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()
rows := sqlmock.NewRows([]string{"user_id", "created_at", "updated_at"}).AddRow(testdata.UserId, time.Now(), time.Now())
// テストしたいクエリ
query := "SELECT * FROM `users` WHERE user_id = ? LIMIT ?"
mock.ExpectQuery(regexp.QuoteMeta(query)).WithArgs("1").WillReturnRows(rows)
if err = mock.ExpectationsWereMet(); err != nil {
t.Errorf("Test failed: %v", err)
}
今回 WithArgs("1")
を与えることで、WHERE user_id = ?
が WHERE user_id = '1'
として評価されます。
これ自体は問題がないのですが、たとえば user_id がユニークな ID、例として UUIDv4 のような毎回ユニークな値だった場合は WithArgs("14608bc0-803a-2a74-a349-36cbada1fdda")
のように毎回 Args を指定しないといけません。
しかし、UUIDv4 で user_id を生成している場合、当然ランダムかつユニークな値を Args として指定しなければならず、テストコードに固定の Args を書いておくことはできません。
そこで、ランダムな任意の文字でも Args として与えられるようにします。
sqlmock での arguments のカスタマイズ
sqlmock では Struct のカスタマイズができるように、Interface が定義されています。
公式の README では time.Time
を使った arguments の Match をカスタマイズする例が載っています。
type AnyTime struct{}
// Match satisfies sqlmock.Argument interface
func (a AnyTime) Match(v driver.Value) bool {
_, ok := v.(time.Time)
return ok
}
func TestAnyTimeArgument(t *testing.T) {
t.Parallel()
db, mock, err := sqlmock.New()
if err != nil {
t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()
mock.ExpectExec("INSERT INTO users").
WithArgs("john", AnyTime{}).
WillReturnResult(sqlmock.NewResult(1, 1))
_, err = db.Exec("INSERT INTO users(name, created_at) VALUES (?, ?)", "john", time.Now())
if err != nil {
t.Errorf("error '%s' was not expected, while inserting a row", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
上記コードは https://github.com/DATA-DOG/go-sqlmock#matching-arguments-like-timetime
より引用しています
そこで、_, ok := v.(time.Time)
の部分を string
に変えた、AnyString struct を定義します。
コードは以下です。
type AnyString struct{}
// Match satisfies sqlmock.Argument interface
func (a AnyString) Match(v driver.Value) bool {
_, ok := v.(string)
return ok
}
この AnyString を定義することで、冒頭のテストコードも次のように書き換えられます。
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()
rows := sqlmock.NewRows([]string{"user_id", "created_at", "updated_at"}).AddRow(testdata.UserId, time.Now(), time.Now())
// テストしたいクエリ
query := "SELECT * FROM `users` WHERE user_id = ? LIMIT ?"
mock.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(AnyString{}).WillReturnRows(rows)
if err = mock.ExpectationsWereMet(); err != nil {
t.Errorf("Test failed: %v", err)
}
WithArgs(AnyString{})
で固定の user_id ではなく、AnyString{}
を渡しています。
これで WHERE user_id = ?
に UUID のランダムな値が来てもエラーが返らないようになります。
以上です。