🕌
GoのDBテスト(MySQL)を並列化して実行時間を短縮する
課題
Goで書かれたWebアプリケーションで、RDBMSはMySQLを使っており、自動テスト(Goテスト)ではモックではなくMySQLを使っている。
実行時間がローカル・リモートで100〜200秒ほどかかっていた。
結果
ソースコードの変更のみで、ローカルでは約1/6、リモートで約1/8まで実行時間を短縮できた。
環境
- 言語: Go1.24
- RDB: MySQL 8.0
既存テスト
- データベースは1つ
- コネクションはテストケースで共通
- テストケース毎にマイグレーションやシード・終了時にテーブルのレコード全削除
- テストケース毎に直列に実行
サンプルコード
package datastore
import (
"database/sql"
"fmt"
"os/exec"
"sync"
"testing"
_ "github.com/go-sql-driver/mysql"
"github.com/google/uuid"
mysqltest "github.com/lestrrat-go/test-mysqld"
migrate "github.com/rubenv/sql-migrate"
)
var (
testMysqld *mysqltest.TestMysqld
templateDB string
templateOnce sync.Once
)
func TestMain(m *testing.M) {
var err error
testMysqld, err = mysqltest.NewMysqld(nil)
if err != nil {
panic(fmt.Sprintf("failed to start mysqld: %v", err))
}
defer testMysqld.Stop()
m.Run()
}
type DBConn struct {
DB *sql.DB
}
// initTemplateDB はマイグレーション済みのテンプレートDBを一度だけ作成する
func initTemplateDB(t *testing.T) {
t.Helper()
templateOnce.Do(func() {
// mysqlデータベースに接続
rootDB, err := sql.Open("mysql", buildDSN("mysql"))
if err != nil {
t.Fatalf("failed to open mysql: %v", err)
}
defer rootDB.Close()
// テンプレートDB作成
templateDB = "template_testdb"
if _, err = rootDB.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", templateDB)); err != nil {
t.Fatalf("failed to create template db: %v", err)
}
// テンプレートDBに接続してマイグレーション実行
db, err := sql.Open("mysql", buildDSN(templateDB))
if err != nil {
t.Fatalf("failed to open template db: %v", err)
}
defer db.Close()
if _, err = migrate.Exec(db, "mysql", migrate.FileMigrationSource{Dir: "../migrations"}, migrate.Up); err != nil {
t.Fatalf("failed to run migrations on template db: %v", err)
}
})
}
func buildDSN(dbname string) string {
return testMysqld.DSN(
mysqltest.WithDbname(dbname),
mysqltest.WithUser("root"),
mysqltest.WithPassword(""),
mysqltest.WithPort(0),
) + "?parseTime=true&loc=Asia%2FTokyo"
}
func restoreDatabase(t *testing.T, dbName string) {
t.Helper()
sock := testMysqld.Socket()
dumpCmd := exec.Command("mysqldump", "--socket="+sock, "-u", "root", templateDB)
restoreCmd := exec.Command("mysql", "--socket="+sock, "-u", "root", dbName)
// dumpの標準出力をrestoreの標準入力に接続
pipe, err := dumpCmd.StdoutPipe()
if err != nil {
t.Fatalf("failed to create pipe: %v", err)
}
restoreCmd.Stdin = pipe
if err := dumpCmd.Start(); err != nil {
t.Fatalf("failed to start mysqldump: %v", err)
}
if err := restoreCmd.Start(); err != nil {
t.Fatalf("failed to start mysql restore: %v", err)
}
if err := dumpCmd.Wait(); err != nil {
t.Fatalf("mysqldump failed: %v", err)
}
if err := restoreCmd.Wait(); err != nil {
t.Fatalf("mysql restore failed: %v", err)
}
}
// SetUpDB は呼び出し毎にデータベースを作成し、任意のシードクエリを実行してから接続を返す。
// 作成するデータベースはパフォーマンス向上のためにテンプレートDBからリストアする。
// example:
//
// func TestSomething(t *testing.T) {
// t.Parallel()
// ctx := context.Background()
//
// query := "INSERT INTO xxx (...) VALUES (...);"
// db := SetUpDB(t, query)
// sut := DataStore{}
// sut.Something(ctx, db)
// }
func SetUpDB(t *testing.T, seeds ...string) *DBConn {
t.Helper()
if testMysqld == nil {
t.Fatalf("mysqld is not initialized")
}
initTemplateDB(t)
// 呼び出し毎にデータベース作成
dbName := "testdb_" + uuid.NewString()
rootDB, err := sql.Open("mysql", buildDSN("mysql"))
if err != nil {
t.Fatalf("failed to open mysql: %v", err)
}
defer rootDB.Close()
if _, err = rootDB.Exec(fmt.Sprintf("CREATE DATABASE `%s`", dbName)); err != nil {
t.Fatalf("failed to create db: %v", err)
}
// テンプレートDBからリストア
restoreDatabase(t, dbName)
// 作成したDBに接続
db, err := sql.Open("mysql", buildDSN(dbName))
if err != nil {
t.Fatalf("failed to open test db: %v", err)
}
// シードデータを作成
for _, seed := range seeds {
if _, err := db.Exec(seed); err != nil {
t.Fatalf("failed to execute seed (%s): %v", seed, err)
}
}
t.Cleanup(func() {
db.Close()
if rootDB, err := sql.Open("mysql", buildDSN("mysql")); err == nil {
rootDB.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS `%s`", dbName))
rootDB.Close()
}
})
return &DBConn{
DB: db
}
}
課題・やってないこと
- テーブル数やテストケース数のスケール:現時点ではテーブル数30程度だが、全テーブルマイグレーション済みのデータベースをテンプレート化していることがボトルネックになるかもしれない
- テスト実行分割: 一番時間がかかるパッケージとそれ以外でgo testコマンドを分割することで、よりCPUを効率的に使えるかもしれない
- データベースは使い捨てではなく、使い終わったデータベースを再利用した方が効率的かもしれない
Discussion