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