前提
Perlというか、周りの人たちだけなのか、はたまた所属している会社の文化なのかもしれないんですけれど、MySQLを使うWebアプリケーションのテストを書くときに、それ専用のDBを立てるわけでございます。都度立てると重いし、上げたり下げたりスキーマ流しこんだりとかまあそのへんのケアが必要なんですけれど、モックとかやらなくていいし、インデックス張ってなくて重いとかもまあ検知できて便利。賛否両論はあるとは思います。
さて、その時に使うTest::mysqldというモジュールがあり非常に便利なわけですが、さてGoのMySQLを使うWebアプリケーションを書く際に、似たような感じでテスト書けないかなと思っていろいろやってみた次第です。
Go触りはじめてあんまり時間立ってないのでそれ違うとかあればコメントなどで教えていただきたいと思っております。
材料
- Go 1.4以降
後述する
func TestMain(m *testing.M)
を使うため - github.com/lestrrat/go-test-mysqld lestrratさん作のTest::mysqldをGoに移植した物です
やり方
めっちゃ雑なコードでそのままは使えないと思うんですけれど、こんな感じ
package hoge_test import ( "testing" "log" "database/sql" "os" "github.com/lestrrat/go-test-mysqld" _ "github.com/go-sql-driver/mysql" ) var testMysqld *mysqltest.TestMysqld func TestMain(m *testing.M) { os.Exit(runTests()) } func runTests(m *testing.M) int { mysqld, err := mysqltest.NewMysqld(nil) if err != nil { log.Fatal("runTests: failed launch mysql server:", err) } defer mysqld.Stop() testMysqld = mysqld return m.Run() } func truncateTables() { db, err := sql.Open("mysql", mysqld.Datasource("test", "", "", 0)) if err != nil { log.Fatal("db connection error:", err) } defer db.Close() rows, err := db.Query("SHOW TABLES") if err != nil { log.Fatal("show tables error:", err) } defer rows.Close() for rows.Next() { var tableName string err = rows.Scan(&tableName) if err != nil { log.Fatal("show table error:", err) } _, err = db.Exec("TRUNCATE " + tableName) if err != nil { log.Fatal("truncate table error:", err) } } } func TestHoge(t *testing.T) { defer truncanteTables() // do something }
解説
go-test-mysqldの使い方
ドキュメント通りにやっているだけなんですが
mysqld, err := mysqltest.NewMysqld(nil)
とやると適当なunix domain socketを作ってくれてmysqlサーバが立ってくれます(skip-networking=trueの場合)。
mysqld.Datasource(dbname string, user string, pass string, port int) string
がDSNを吐いてくれるのでdatabase/sql
でつなげます。
func TestMain(m *testing.M)で安全に終了する
go-test-mysqldにはStop()
というメソッドがあり、こいつを叩くとmysqlサーバが落ちてibdataなども掃除してくれます。
というわけで他の言語のテストフレームワークで言うsetup
でmysqlサーバを起動し、teardown
でシャットダウンと掃除をやって欲しい感じがいたします。
というわけでGo 1.4のtesting
から使えるようになったTestMain(m *testing.M)
という関数をテストファイルに書くことで、テストを実行する前後にやることを記述することが出来ます。
ドキュメントではTestMain(m *testing.M)
の中でos.Exit(status int)
を呼べと書いてあり、m.Run() int
はステータスコードを返してくれるっぽいので、ミニマムな実装では、
func TestMain(m *testing.M) {
os.Exit(m.Run())
}
となります。これもドキュメント通りですが。
だったら
func TestMain(m *testing.M) { mysqld, err := mysqltest.NewMysqld defer mysqld.Stop() os.Exit(m.Run()) }
でええやんとなりますが、os.Exit()
はdeferに登録されているやつを破棄して終わるという罠がございまして、掃除されなくてmysqldのプロセスも残ってアレアレ? となっていたのでした。この罠前もハマった気がいたします。。。
というわけでmysqldを立ち上げてRunだけする関数を渡してやってその中でdeferするのがいいかなと思いました。
テスト毎にデータをお掃除
truncateTabls()
なる怖そうな関数をテスト毎にdeferして、毎回クリーンな状態にしてるみたいなこともしています。
そうするとテスト間の依存がなくなる(ハズ)なのでテストが書きやすいというわけです。
ただままこれも時間かかるし、場合によりけりかなってお思うけれど、Perlでこういうスタイルでテストを書いていってるのでこうなっているという感じです。
参考
その他
あとmysqldを作ったあとでスキーマ突っ込んだりとか、場合によってはマスタデータなんかも入れてもいいと思います。消してー入れてーを繰り返すとこれまたテストが長くなるという弊害もありますが。 あとそれから、_test.goファイル毎にmysqldが立ったりしてオッオッとなるので、そこらへんは仕方ないけれど工夫の余地はあるかなーと思いました。
なおこれらの問題への解決としてPerlには
- datadirをコピーして初期データ作成の時間を短縮する Test::mysqld の
copy_data_from
- mysqldをpoolingする App::Prove::Plugin::MySQLPool
- dockerでmysqldを立てることによって初期データの作成などの時間を節約する Test::Docker::MySQL
などのソリューションがあります。 あとたぶんマルチスレッドとかあんまり考えていないのでそのへんでバグるかも。