0
0

sqlmock に AnyString で任意の文字を許可させる

Last updated at Posted at 2024-08-17

概要 と 解説

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 のランダムな値が来てもエラーが返らないようになります。

以上です。

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