GolangでDBアクセスがあるユニットテストのやり方を考える - Qiita
-
SQLが実行される箇所をmockする(実際にDBに接続してSQLの結果を得る必要がない場合)
- sqlmock を使う
-
実際のDBとテストデータを用意してSQLも実際に実行する
-
テスト用DBの用意
- https://github.com/DATA-DOG/go-txdb でテストケースごとに独立したトランザクション内でテストデータを用意・Rollbackを行う
- https://zenn.dev/rinchsan/articles/83cf6f3b5d70c4d9b5d4
- mysql だとnested transactionが使えないからコミットしている場合、Savepointにかえるとかしないと、rollbackできなさそう…?
- 毎回deleteする、でもよくない?
- https://github.com/go-testfixtures/testfixtures でfixtureを用意する
- https://github.com/testcontainers/testcontainers-go や https://github.com/ory/dockertest を利用する
- https://qiita.com/yasuflatland-lf/items/4f18b55c2a6492d0c462
- Makefileでdocker-compose up してもいいけど、テストコードで書いておくとSkipも可
- https://github.com/DATA-DOG/go-txdb でテストケースごとに独立したトランザクション内でテストデータを用意・Rollbackを行う
-
CIではスキップしたい
func skipCI(t *testing.T) {
if os.Getenv("CI") != "" {
t.Skip("Skipping testing in CI environment")
}
}
func TestNewFeature(t *testing.T) {
skipCI(t)
}
あるいはshortではスキップにする go test -short
if testing.Short() {
t.Skip("skipping testing in short mode")
}
dockertestとfixtureを使ったテスト
dockertest を使ってコンテナを起動し、testfixtures でテストデータを流し込む
package db_test
import (
"database/sql"
"fmt"
"os"
"testing"
"time"
"github.com/go-testfixtures/testfixtures/v3"
"github.com/ory/dockertest/v3"
)
var (
pool *dockertest.Pool
resource *dockertest.Resource
db *sql.DB
fixtures *testfixtures.Loader
)
func prepareTestDatabase() error {
var err error
pool, err = dockertest.NewPool("")
// コンテナの起動に時間がかかるため
pool.MaxWait = 2 * time.Minute
if err != nil {
return err
}
basepath, _ := os.Getwd()
user := "user"
// DB名に test が入っていないと、fixtureのload時にエラーになる (https://github.com/go-testfixtures/testfixtures#security-check)
dbName := "test_db"
// Dockerコンテナ起動時の細かいオプションを指定する
runOptions := &dockertest.RunOptions{
Repository: "mysql",
Tag: "5.7",
Env: []string{
fmt.Sprintf("MYSQL_DATABASE=%s", dbName),
fmt.Sprintf("MYSQL_USER=%s", user),
fmt.Sprintf("MYSQL_PASSWORD=%s", user),
"MYSQL_ROOT_PASSWORD=secret",
"TZ=Asia/Tokyo",
},
Mounts: []string{
basepath + "/etc/01_create_table.sql:/docker-entrypoint-initdb.d/01_create_table.sql",
},
Cmd: []string{
"mysqld",
"--character-set-server=utf8",
"--collation-server=utf8_unicode_ci",
},
}
// imageをpullして起動する
resource, err = pool.RunWithOptions(runOptions)
if err != nil {
return err
}
// コンテナが起動されたことを確認するためのコマンドを定義
if err := pool.Retry(func() error {
time.Sleep(10 * time.Second)
err := connectDb(resource.GetPort("3306/tcp"))
if err != nil {
return err
}
return db.Ping()
}); err != nil {
return err
}
return nil
}
func connectDb(port string) error {
dsn := fmt.Sprintf("%s:%s@tcp(localhost:%s)/%s?parseTime=true&loc=Asia2FTokyo",
user,
user,
port,
dbName,
)
dbURL := func(port nat.Port) string {
return dsn
}
execError := compose.
WaitForService("db", wait.ForSQL(nat.Port(port), "mysql", dbURL)).
WithCommand([]string{"up", "-d"}).
Invoke()
err := execError.Error
if err != nil {
return fmt.Errorf("Could not run compose file: %v - %v", composeFilePaths, err)
}
db, err = sql.Open("mysql", dsn)
if err != nil {
return err
}
return db.Ping()
}
func TestSample(t *testing.T) {
// テストデータ準備 start
if testing.Short() {
t.Skip("skip integration test")
}
var err error
err = prepareContainer(t)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := compose.Down().Error; err != nil {
t.Fatalf("Could not purge resource: %v", err)
}
}()
// テストデータ準備 end
// ... test
}