読者です 読者をやめる 読者になる 読者になる

ぱいぱいにっき

Pythonが好きすぎるけれど、今からPerlを好きになりますにっき

GoでMySQLを使ったテストをする

前提

Perlというか、周りの人たちだけなのか、はたまた所属している会社の文化なのかもしれないんですけれど、MySQLを使うWebアプリケーションのテストを書くときに、それ専用のDBを立てるわけでございます。都度立てると重いし、上げたり下げたりスキーマ流しこんだりとかまあそのへんのケアが必要なんですけれど、モックとかやらなくていいし、インデックス張ってなくて重いとかもまあ検知できて便利。賛否両論はあるとは思います。

さて、その時に使うTest::mysqldというモジュールがあり非常に便利なわけですが、さてGoのMySQLを使うWebアプリケーションを書く際に、似たような感じでテスト書けないかなと思っていろいろやってみた次第です。

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なども掃除してくれます。 というわけで他の言語のテストフレームワークで言うsetupmysqlサーバを起動し、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でこういうスタイルでテストを書いていってるのでこうなっているという感じです。

参考

  • qiita.com

  • qiita.com

その他

あとmysqldを作ったあとでスキーマ突っ込んだりとか、場合によってはマスタデータなんかも入れてもいいと思います。消してー入れてーを繰り返すとこれまたテストが長くなるという弊害もありますが。 あとそれから、_test.goファイル毎にmysqldが立ったりしてオッオッとなるので、そこらへんは仕方ないけれど工夫の余地はあるかなーと思いました。

なおこれらの問題への解決としてPerlには

  • datadirをコピーして初期データ作成の時間を短縮する Test::mysqld の copy_data_from
  • mysqldをpoolingする App::Prove::Plugin::MySQLPool
  • dockerでmysqldを立てることによって初期データの作成などの時間を節約する Test::Docker::MySQL

などのソリューションがあります。 あとたぶんマルチスレッドとかあんまり考えていないのでそのへんでバグるかも。