go-sqlmock
とは
go-sqlmock
は Go の database/sql/driver
の実装で、DB ドライバの振る舞いをモック化できるライブラリ。
go-sqlmock
を使用することで、DB ドライバを必要するロジックと、実際の DB ドライバ以降の処理を分離してテストできる。
go-sqlmock
の特徴
DB ドライバに対するクエリ単位でモック設定ができる
sql.DB::Query
や sql.DB::Exec
に指定する個々のクエリの内容ごとに返戻する行や結果を指定できる。
複数パターンのクエリに対して同一の結果を返したい場合も、正規表現で指定可能。
DB ドライバに対する関数呼び出しの順序検証ができる
ドライバのモック化だけでなく、ドライバモックに対して呼ばれたクエリやトランザクション処理の順序を検証する機能も持つ。
その他特徴
- 安定版である
- 並行性と同時接続サポート
- Go1.8 以降の Context に対応:SQL パラメータの命名とモッキング
- 仕様元ソースコードの変更必要なし
- どの
sql/driver
メソッドの振る舞いもモック化できる - 厳密な期待順序マッチングを搭載
- サードパーティライブラリへの依存なし
Usage
Install
$ go get github.com/DATA-DOG/go-sqlmock
検証のため、今回は MySQL ドライバを使用。
$ go get github.com/go-sql-driver/mysql
検証用コード
今回使用する検証用コードは以下に置いてあります。
rennnosuke/go-playground/go-sqlmock
取得クエリ(SELECT)の検証
テスト対象コード
DB ドライバを使用するコードを用意する。
import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) type Article struct { ID int Title string Content string } func GetByID(id int, db *sql.DB) (*Article, error) { row := db.QueryRow("SELECT * FROM ARTICLES WHERE ID = ? AND IS_DELETED = 0", id) e := Article{} if err := row.Scan(&e.ID, &e.Title, &e.Content); err != nil { return nil, fmt.Errorf("failed to scan row: %s", err) } return &Article{ID: e.ID, Title: e.Title, Content: e.Content}, nil }
テストコード
ドライバを使用する実装に対する、go-sqlmock
を使用したテストコードを用意。
import ( "fmt" "testing" "github.com/DATA-DOG/go-sqlmock" ) func TestSQLMock_Select(t *testing.T) { // モックDBの初期化 db, mock, err := sqlmock.New() if err != nil { t.Fatalf("failed to init db mock") } defer db.Close() id := 1 // dbドライバに対する操作のモック定義 columns := []string{"id", "title", "content"} mock.ExpectQuery("SELECT (.+) FROM ARTICLES"). // expectedSQL: 想定される実行クエリをregexpで指定(指定文字列が含まれるかどうかを見る) WithArgs(id). // 想定されるプリペアドステートメントへの引数 WillReturnRows(sqlmock.NewRows(columns).AddRow(1, "test title", "test content")) // 返戻する行情報の指定 // テスト対象関数call article, err := GetByID(id, db) if err != nil { t.Fatalf("failed to get article: %s", err) } fmt.Printf("%v", article) // mock定義の期待操作が順序道理に実行されたか検査 if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("failed to ExpectationWerMet(): %s", err) } }
モック DB の初期化
sqlmock.New()
ファクトリ関数で sql.DB
値への参照と、モックを定義するための SqlMock
型の値を取得できる。
sql.DB
は DB ドライバを使用するロジックに引き渡し、DB ドライバ以降の処理のモックとして使用される。
SqlMock
は主にテスト関数内で、想定される DB ドライバに対する操作と、それらに対する結果を定義する。
// モックDBの初期化
db, mock, err := sqlmock.New()
ドライバへの想定操作と結果の定義
次に、DB ドライバに対してどのようなクエリが実行されるのかを SqlMock::ExpectQuery()
関数で定義する。
この定義は後述する SqlMock::ExpectationsWereMet()
の検証で使用される。
指定するクエリ文字列は regexp
パッケージ準拠の正規表現で記述できる。またデフォルト設定の場合、指定した文字列が実際に呼び出されたクエリに部分マッチすれば検証は Pass される。
クエリにプリペアドステートメントが指定されている場合、WithArts
で実際に挿入する値を指定できる。
また、各々の操作に対してどのような結果を返すのかを SqlMock::WillReturnRows()
で定義している。
返す行のカラムを NewRows()
で、行データを *Rows.AddRow()
で指定している。
// dbドライバに対する操作のモック定義 columns := []string{"id", "title", "content"} mock.ExpectQuery("SELECT (.+) FROM ARTICLES"). // expectedSQL: 想定される実行クエリをregexpで指定(指定文字列が含まれるかどうかを見る) WithArgs(id). // 想定されるプリペアドステートメントへの引数 WillReturnRows(sqlmock.NewRows(columns).AddRow(1, "test title", "test content")) // 返戻する行情報の指定
実際のクエリ呼び出し検証
ExpectationsWereMet checks whether all queued expectations were met in order. If any of them was not met - an error is returned.
SqlMock::ExpectationsWereMet()
は、呼び出した SqlMock::ExpectXXX
に即する操作が、実際に DB ドライバ( sqlmock.New()
から返戻された sql.DB
)に順番どおりに実行されたかどうかを検証する。
// mock定義の期待操作が順序道理に実行されたか検査 if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("failed to ExpectationWerMet(): %s", err) }
永続化処理(INSERT)の検証
テスト対象コード
func Create(id int, title, content string, db *sql.DB) error { tx, err := db.Begin() defer func() { switch err { case nil: tx.Commit() default: tx.Rollback() } }() if err != nil { return err } _, err = tx.Exec("INSERT INTO ARTICLES (ID, TITLE, CONTENT) VALUES (?, ?, ?)", id, title, content) if err != nil { return fmt.Errorf("failed to insert article: %s", err) } return nil }
テストコード
func TestSQLMock_Insert(t *testing.T) { // モックDBの初期化 // ... id := 1 title := "test title" content := "test content" // dbドライバに対する操作のモック定義 mock.ExpectBegin() mock.ExpectExec("INSERT INTO ARTICLES"). // 想定される実行SQLをregexpで指定(指定文字列が含まれるかどうかを見る) WithArgs(id, title, content). // 想定されるプリペアドステートメントへの引数 WillReturnResult(sqlmock.NewResult(1, 1)) // 想定されるExec関数の結果を指定 mock.ExpectCommit() // テスト対象関数call // ... // mock定義の期待操作が順序道理に実行されたか検証 // ... }
ドライバへの想定操作と結果の定義
SELECT
検証時と異なる点は 2 つ。
トランザクション呼び出しの定義 永続化処理などでトランザクション開始終了処理
Begin()
Commit()
RollBack()
を呼ぶとき、それに合わせてSqlMock.ExpectBegin()
SqlMock.ExpectCommit()
SqlMock.ExpectRollback()
を呼ぶ。Exec 関数呼び出しの定義 永続化処理では
sql.DB::Query()
ではなくsql.DB::Exec()
を使用しているため、SqlMock.ExpectExec()
を使用している。また返戻値が行ではなく更新結果となるため、これをWillReturnResult()
で指定し、返戻値の内容をNewResult()
で定義している。
// dbドライバに対する操作のモック定義 mock.ExpectBegin() mock.ExpectExec("INSERT INTO ARTICLES"). // 想定される実行SQLをregexpで指定(指定文字列が含まれるかどうかを見る) WithArgs(id, title, content). // 想定されるプリペアドステートメントへの引数 WillReturnResult(sqlmock.NewResult(1, 1)) // 想定されるExec関数の結果を指定 mock.ExpectCommit()
UPDATE
処理を実装した場合も、基本検証は INSERT
とほぼ同じ流れになる。
クエリマッチングルールの変更
SqlMock::ExpectQuery()
SqlMock::ExpectExec()
でクエリに含まれるべき文字列を指定する事ができたが、このルールは sqlmock.New()
に sqlmock.QueryMatcherOption()
返戻値を指定することで変更可能になる。
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
sqlmock.QueryMatcherOption()
には以下の QueryMatcher が指定できる。
sqlmock.QueryMatcherRegexp
: 想定される SQL 文字列を正規表現として使用し、実行クエリの文字列と照合するsqlmock.QueryMatcherEqual
: 想定される SQL 文字列と実行クエリを大文字・小文字含め完全一致で照合する
この QueryMatcher は QueryMatcherFunc
型で、この型にキャスト可能な関数を定義すれば独自の QueryMatcher を定義できる。
type QueryMatcherFunc func(expectedSQL, actualSQL string) error
所感
- テストケースごとに個別にクエリ結果を設定できるのは柔軟でよい
sql.DB
関数呼び出し順検証、DB 周りの処理が頻繁に変更しない前提ならば便利かも- 逐一想定クエリを記述するのはちょっと面倒
- RDB のテーブルスキーマやリレーションは再現できないので、実動環境と乖離しない定義を書くことが求められる1