Top View


Author shiro seike / せいけ しろう / 清家 史郎

DATA-DOG/go-sqlmockを使ってGormDBをmockする

2020/12/02

mockを作成

sql-mockはsqlmock.New()という関数を持っており、
そちらを利用するとテスト関数に渡すDBと振る舞いを定義出来るmockを返してくれます。
さらに今回Gormを使用したいので、そのDBに対してgorm用のコネクションを作成しておきます。
僕はPostgreSQLを利用していたので、PostgreSQL用のコネクションを生成します。

func GetNewDbMock() (*gorm.DB, sqlmock.Sqlmock, error) {
    db, mock, err := sqlmock.New()
    if err != nil {
        return nil, mock, err
    }

    gormDB, err := gorm.Open(
        postgres.New(
            postgres.Config{
                Conn: db,
            }), &gorm.Config{})

    if err != nil {
        return gormDB, mock, err
    }

    return gormDB, mock, err
}

各テスト用のメソッドではこのGormDBとmockを利用して振る舞いをテストしていきます。

SELECT系

SELECT系のテストを行うときは mock.ExpectQueryを利用します。
その振る舞いをチェックするにあたり、一致するようなSQLを記述する必要があるのですが、
わりと面倒なので、僕の場合はいい加減なSQLをまず実行します。

ここでは hogeというQueryを投げてしまいます。

あとは実際に実行したいDBに対して、テスト対象の関数を通してみます。
今回はTagテーブルに対してSELECTを行うGetTagのテストを行います。

func TestGetTag(t *testing.T) {
    db, mock, err := GetNewDbMock()
    if err != nil {
        t.Errorf("Failed to initialize mock DB: %v", err)
        return
    }

    mock.ExpectQuery(regexp.QuoteMeta(`hoge`))

    res, err := GetTag(db, 1)

    assert.Equal(t, err, nil)
    assert.NotEqual(t, res, nil)
}
$ go test ./...

****/models/tag_func.go:12 Query: could not match actual sql: "SELECT * FROM "tags" WHERE id = $1" with expected regexp "hoge"
[0.041ms] [rows:0] SELECT * FROM "tags" WHERE id = 1
--- FAIL: TestGetTag (0.00s)
    tag_func_test.go:21:
                Error Trace:    tag_func_test.go:21
                Error:          Not equal:
                                expected: *errors.errorString(&errors.errorString{s:"Query: could not match actual sql: \"SELECT * FROM \"tags\" WHERE id = $1\" with expected regexp \"hoge\""})
                                actual  : <nil>(<nil>)
                Test:           TestGetTag

するとこのmockが期待しているhogeと実際の関数が発行しているSQL
SELECT * FROM "tags" WHERE id = 1
が違うということを教えてくれます。
そのSQLが自分が期待している振る舞いである場合は、そちらのSQLを記述していきます。

  func TestGetTag(t *testing.T) {
      db, mock, err := GetNewDbMock()
      if err != nil {
          t.Errorf("Failed to initialize mock DB: %v", err)
          return
      }

      var id int64 = 1
      var name string = "tag1"

      mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "tags" WHERE id = $1`)).
          WithArgs(id).
          WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(id, name))

      res, err := GetTag(db, id)

      assert.Equal(t, err, nil)
      assert.Equal(t, *res.ID, id)
      assert.Equal(t, res.Name, name)

      if err := mock.ExpectationsWereMet(); err != nil {
          t.Errorf("TestGetTag: %v", err)
      }
  }

WithArgs(id) でクエリパラメータの指定
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(id, name))で実際に返答を期待するレコードを記述します。

すると SELECT * FROM "tags" WHERE id = 1という期待されたSQLが提供されたので、
DB側に id=1, name=tag1のデータを用意してくれた状態になり、GetTagではそちらの結果が返されます。

結果、当たり前なのですがその後のassert.Equalは正しい結果が入っていることが確認出来て、
最後にmock.ExpectationsWereMetにて期待通りの実行結果になっているかを検証出来ます。

INSERT、UPDATE、DELETE系

更新系のテストを行うときは通常 mock.ExpectedExec を利用すると思います。

ただし、Gormを使う場合はGormと同様のSQLをmock.ExpectedExecで再現する方法がわからず、
mock.ExpectQuery を利用しています。

SELECTと同じように振る舞いをチェックするにあたり、一致するようなSQLを記述する必要があるのですが、
同じようにいい加減なSQLをまず実行します。

SELECTと同じようにhogeというSQLを投げてしまいます。

  func TestCreateTag(t *testing.T) {
      db, mock, err := GetNewDbMock()
      if err != nil {
          t.Errorf("Failed to initialize mock DB: %v", err)
          return
      }

      // Mock設定
      mock.ExpectQuery(`hoge`)
      res, err := CreateTag(db, "tag1")

      assert.Equal(t, *res.ID, id)
      assert.Equal(t, res.Name, name)

  }
call to Query 'INSERT INTO "tags" ("name") VALUES ($1) RETURNING "id"' with args [{Name: Ordinal:1 Value:tag1}], was not expected, next expectation is: ExpectedExec => expecting Exec or ExecContext which:
  - matches sql: 'hoge'
  - is without arguments
[0.044ms] [rows:0] INSERT INTO "tags" ("name") VALUES ('tag1') RETURNING "id"
--- FAIL: TestCreateTag (0.00s)
    tag_func_test.go:134:
                Error Trace:    tag_func_test.go:134
                Error:          Not equal:
                                expected: *errors.errorString(&errors.errorString{s:"call to Query 'INSERT INTO \"tags\" (\"name\") VALUES ($1) RETURNING \"id\"' with args [{Name: Ordinal:1 Value:tag1}], was not expected, next expectation is: ExpectedExec => expecting Exec or ExecContext which:\n  - matches sql: 'hoge'\n  - is without arguments"})
                                actual  : <nil>(<nil>)
                Test:           TestCreateTag

そのSQLが自分が期待している振る舞いである場合は、そちらのSQLを記述していきます。

  func TestCreateTag(t *testing.T) {
      db, mock, err := GetNewDbMock()
      if err != nil {
          t.Errorf("Failed to initialize mock DB: %v", err)
          return
      }

      var id int64 = 1
      var name string = "tag1"

      // Mock設定
      mock.ExpectBegin()
      mock.ExpectQuery(`INSERT INTO "tags" (.+) RETURNING`).
          WithArgs(name).
          WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id))
      mock.ExpectCommit()

      res, err := CreateTag(db, "tag1")

      assert.Equal(t, *res.ID, id)
      assert.Equal(t, res.Name, name)

      if err := mock.ExpectationsWereMet(); err != nil {
          t.Errorf("TestCreateTag: %v", err)
      }
  }

これで一通りのSQLがmockを介してテスト出来るようになりました。

Model系のテストが通るようにしておくと、
変更に強いアプリケーションの構築の一助となりますので
実行コストの低いテストをしっかり記述していきたいところです。

shiro seike / せいけ しろう / 清家 史郎

shiro seike / せいけ しろう / 清家 史郎

Twitter X

Company:Fusic CO., LTD. Slides:slide.seike460.com blog:blog.seike460.com Program Language:PHP , Go Interest:Full Serverless Architecture

Related Posts

sam local start-apiでDynamoDB Localをゆるふわに使う
shiro seike / せいけ しろう / 清家 史郎

shiro seike / せいけ しろう / 清家 史郎

2020/12/30


aws-sdk-goを使ったAthena API
shiro seike / せいけ しろう / 清家 史郎

shiro seike / せいけ しろう / 清家 史郎

2020/12/21




golang.org/x/text/message でi18n対応を行う
shiro seike / せいけ しろう / 清家 史郎

shiro seike / せいけ しろう / 清家 史郎

2019/12/22