GoでDBのテストにgithub.com/cockroachdb/copyistを使う
久しぶりにMySQLを使ったシステムを触ることになったものの、現状のテストが遅すぎたので高速化に取り組むことにした。
遅い原因としては、
- テストでDBを使うパッケージが多いので接続時間がそれなりになってしまう
- テストケースごとにTRUNCATE→INSERTでデータを入れ直している
といったところ。
なので今回はDBにアクセスしないでテストができるようになるcopyistを試してみようと思う。
仕組みとしては実行されたSQLを記録しておき、以降はその時のデータを使うという
などと似たようなものとなる。
README.mdにはMySQLは非対応と書いてあったが、普通に使うことができた。
(もしかしたら一部正しく記録/再生できないクエリがあるのかもしれない)
MySQL版のサンプルコードはこちら。
mysql_test.go
package example import ( "database/sql" "log" "testing" "github.com/cockroachdb/copyist" _ "github.com/go-sql-driver/mysql" ) // GetName をテストする。 func GetName(db *sql.DB, id int64) (string, error) { var name string err := db.QueryRow("SELECT name FROM `user` WHERE id=?", id).Scan(&name) return name, err } // resetDBで使うためにグローバル変数にしておく。 var db *sql.DB // 記録時にcopyist.Open()の中で実行される。 func resetDB() { db.Exec("DROP TABLE `user`") db.Exec("CREATE TABLE `user` (`id` INT(11) unsigned NOT NULL AUTO_INCREMENT, `name` VARCHAR(20) NOT NULL, PRIMARY KEY (`id`))") db.Exec("INSERT INTO `user` (id, name) VALUES (?, ?)", 1, "Andy") } func TestMain(m *testing.M) { // ドライバ名は"copyist_mysql"になる。 copyist.Register("mysql", resetDB) // resetDBとテストで使うdbを作る。 var err error db, err = sql.Open("copyist_mysql", "admin:pass@/copyist") if err != nil { log.Fatal(err) } defer db.Close() m.Run() } func TestGetName(t *testing.T) { defer copyist.Open(t).Close() // found name, err := GetName(db, 1) if err != nil { t.Fatal(err) } if name != "Andy" { t.Error("failed test") } // not found if _, err := GetName(db, 100); err != sql.ErrNoRows { t.Error(err) } } // subtest版 func TestGetName_subtest(t *testing.T) { t.Run("found", func(t *testing.T) { defer copyist.Open(t).Close() name, err := GetName(db, 1) if err != nil { t.Fatal(err) } if name != "Andy" { t.Error("failed test") } }) t.Run("not found", func(t *testing.T) { defer copyist.Open(t).Close() if _, err := GetName(db, 100); err != sql.ErrNoRows { t.Error(err) } }) }
1回目は以下のように -record
をつけてテストを実行する。
$ go test -v -record === RUN TestGetName --- PASS: TestGetName (0.09s) === RUN TestGetName_subtest === RUN TestGetName_subtest/found === RUN TestGetName_subtest/not_found --- PASS: TestGetName_subtest (0.12s) --- PASS: TestGetName_subtest/found (0.06s) --- PASS: TestGetName_subtest/not_found (0.06s) PASS
そうするとtestdataディレクトに以下のようなmysql_test.copyistが生成される。
1=DriverOpen 1:nil 2=ConnPrepare 2:"SELECT name FROM `user` WHERE id=?" 1:nil 3=StmtNumInput 3:1 4=StmtQuery 1:nil 5=RowsColumns 9:["name"] 6=RowsNext 11:[10:QW5keQ] 1:nil 7=RowsNext 11:[] 7:EOF "TestGetName"=1,2,3,4,5,6,2,3,4,5,7 "TestGetName_subtest/found"=1,2,3,4,5,6 "TestGetName_subtest/not_found"=1,2,3,4,5,7
この状態で -record
をつけずに実行すると↑のデータが使われる。
$ go test -v === RUN TestGetName --- PASS: TestGetName (0.00s) === RUN TestGetName_subtest === RUN TestGetName_subtest/found === RUN TestGetName_subtest/not_found --- PASS: TestGetName_subtest (0.00s) --- PASS: TestGetName_subtest/found (0.00s) --- PASS: TestGetName_subtest/not_found (0.00s) PASS
なお、記録する前に -record
無しで実行するとpanicになる。
$ go test -v === RUN TestGetName --- FAIL: TestGetName (0.00s) panic: no recording exists with this name: TestGetName [recovered] panic: no recording exists with this name: TestGetName
また、以下のようにクエリを変えて、
--- a/mysql_test.go +++ b/mysql_test.go @@ -12,7 +12,7 @@ import ( // GetName をテストする。 func GetName(db *sql.DB, id int64) (string, error) { var name string - err := db.QueryRow("SELECT name FROM `user` WHERE id=?", id).Scan(&name) + err := db.QueryRow("SELECT name FROM `user` WHERE id=? AND TRUE", id).Scan(&name) return name, err }
記録し直さずにそのまま実行してもpanicになる。
$ go test -v === RUN TestGetName --- FAIL: TestGetName (0.00s) panic: mismatched argument to ConnPrepare, expected SELECT name FROM `user` WHERE id=? AND TRUE, got SELECT name FROM `user` WHERE id=? - regenerate recording [recovered] panic: mismatched argument to ConnPrepare, expected SELECT name FROM `user` WHERE id=? AND TRUE, got SELECT name FROM `user` WHERE id=? - regenerate recording
ただし、プレースホルダーの値が変わっても返ってくるレコードは変わらないのでその点は注意が必要。
例えば以下のようにidを逆にしてもテストはPassしてしまう。
--- a/mysql_test.go +++ b/mysql_test.go @@ -45,7 +45,7 @@ func TestGetName(t *testing.T) { defer copyist.Open(t).Close() // found - name, err := GetName(db, 1) + name, err := GetName(db, 100) if err != nil { t.Fatal(err) } @@ -54,7 +54,7 @@ func TestGetName(t *testing.T) { } // not found - if _, err := GetName(db, 100); err != sql.ErrNoRows { + if _, err := GetName(db, 1); err != sql.ErrNoRows { t.Error(err) } } @@ -63,7 +63,7 @@ func TestGetName_subtest(t *testing.T) { t.Run("found", func(t *testing.T) { defer copyist.Open(t).Close() - name, err := GetName(db, 1) + name, err := GetName(db, 100) if err != nil { t.Fatal(err) } @@ -75,7 +75,7 @@ func TestGetName_subtest(t *testing.T) { t.Run("not found", func(t *testing.T) { defer copyist.Open(t).Close() - if _, err := GetName(db, 100); err != sql.ErrNoRows { + if _, err := GetName(db, 1); err != sql.ErrNoRows { t.Error(err) } })
細かい部分はまだ何とも言えないが、とりあえず使えそうだし高速化にも期待ができそう。
さて、あとはこれをどうやって既存のテストに組み込んでいくか。
既存のテストではxormが使われているが、copyistに対応していないので少し特殊な初期化をしないといけないようだ。
engine, err := xorm.NewEngine("mysql", "") // エラーハンドリングやengineの設定など // ... // DBをsql.Open("copyist_mysql", ...)したものと差し替える engine.DB().DB = db
これを何とかするのが一番大変なのかもしれない...