Fusic Tech Blog

Fusion of Society, IT and Culture

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

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

本記事はGo Advent Calendar 2020の2日目の記事 およびFusic Advent Calendar 2020の2日目の記事です。

以前GoでRDBのテストを書く時

  • テスト用のDBを用意しておく
  • テスト実行時CreateTableをする
  • データを挿入する
  • テストを実行する
  • テスト終了時DropTableをする

ということをしてました。

DBの振る舞いをテストするだけならここまでする必要はないなと思っており、実行速度も気になっていました。

そこで今回は普段使っているORMであるGormに対してgo-sqlmockを用いて
GormDBをmockする方法をご紹介します。

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  : ()
                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

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