単体テストをDBインスタンスをどう用意するかという問題に必ず当たるかと思います。
今回は、DBインスタンスを用意する方法としてdocker testを使った方法を紹介したいと思います。
目次
前提
今回の記事では、DBサーバは、Postgresを前提としています。今回、紹介するdocker testでは、MySQLでも同様に利用可能です。
DBインスタンスの準備方法
大きな方針としては、次の2つかと思います。
- mockを使う
- DBインスタンスを使う
Mockを使う方法
こちらの方法は、SQL周りをmockに置き換えて、SQLへアクセスする際のパラメータや挙動を変更してテストを行います。代表的なモックの例としては、go-sqlmockがあります。
単体テスト用のDBを使う方法
DBインスタンスを利用する場合は、大きく分けて2つの方法があると思います。
開発環境で作成しているDBサーバに単体テスト用のDBを作成する
DBインスタンスを伴う開発を行っている場合、多くの場合は、すでに作成中の環境に単体テスト専用のDBを作成することが多いと思います。
別のDBサーバをdockertestで準備する
dockertestを利用した方法になります。こちらは、テスト実行時に実行時にdockerコンテナとしてテスト用のDBサーバを作成するコードを紹介します。testMain()を作成して行います。
package usecases import ( "database/sql" "fmt" "log" "os" "testing" "time" db "appswingby.com/sample/db/sqlc" "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" "github.com/ory/dockertest" "github.com/ory/dockertest/docker" ) var testQueries *db.Queries var ( user = "postgres" password = "secret" dbName = "unittest" port = "5433" dialect = "postgres" dsn = "postgres://%s:%s@localhost:%s/%s?sslmode=disable" ) func createContainer() (*dockertest.Resource, *dockertest.Pool) { // Dockerとの接続 pool, err := dockertest.NewPool("") pool.MaxWait = time.Minute * 2 if err != nil { log.Fatalf("Could not connect to docker: %s", err) } // Dockerコンテナ起動時の細かいオプションを指定する runOptions := &dockertest.RunOptions{ Repository: "postgres", Tag: "12.3", Env: []string{ "POSTGRES_USER=" + user, "POSTGRES_PASSWORD=" + password, "POSTGRES_DB=" + dbName, }, ExposedPorts: []string{"5432"}, PortBindings: map[docker.Port][]docker.PortBinding{ "5432": { {HostIP: "0.0.0.0", HostPort: port}, }, }, } // コンテナを起動 resource, err := pool.RunWithOptions(runOptions) if err != nil { log.Fatalf("Could not start resource: %s", err) } return resource, pool } func closeContainer(resource *dockertest.Resource, pool *dockertest.Pool) { // コンテナの終了 if err := pool.Purge(resource); err != nil { log.Fatalf("Could not purge resource: %s", err) } } func connectDB(resource *dockertest.Resource, pool *dockertest.Pool) *sql.DB { // DB(コンテナ)との接続 var conn *sql.DB if err := pool.Retry(func() error { time.Sleep(time.Second * 10) var err error dsn = fmt.Sprintf(dsn, user, password, port, dbName) conn, err = sql.Open("postgres", dsn) if err != nil { return err } return conn.Ping() }); err != nil { log.Fatalf("Could not connect to docker: %s", err) } return conn } func setupTestDB(conn *sql.DB) (*sql.DB, *migrate.Migrate) { // DBに接続 sqlDir := "file://../db/migrations" m, err := migrate.New(sqlDir, dsn) if err != nil { fmt.Fprintf(os.Stderr, "dsn: %v, sqlDir: %v, error: %v\n", dsn, sqlDir, err) os.Exit(1) } if err := m.Up(); err != nil { if err.Error() == "no change" { log.Println("no change made by migration scripts") } else { log.Fatal(err) } } return conn, m } func makeSeeds (conn *sql.DB) { // テストで利用するデータをInsertしておく } func TestMain(m *testing.M) { // DBの立ち上げ resource, pool := createContainer() defer closeContainer(resource, pool) // DBへ接続する conn := connectDB(resource, pool) defer conn.Close() // テスト用DBをセットアップ setupTestDB(conn) makeSeeds(conn) // 接続情報を渡すとQueryインスタンスを生成 testQueries = db.New(conn) // テスト実行する m.Run() }
この例では、下記のことを行っています。
- dockerコンテナとして、postgresコンテナを立ち上げる。
createContainer() - 作成したコンテナと接続する。
connectDB() - dbへマイグレーションを行う。
setupTestDB() - 初期データを作成する
mkaeSeeds() - sqlcで利用するqueriesをテスト用のqueriesへ置き換える
sqlcについては、以前紹介した記事を参考にして下さい。
テスト実行は、以下のコマンドを実行するだけです。
go test -v -coverprofile=work/cover.out ./...
go tool cover -html=work/cover.out -o work/cover.html
まとめ
今回は、dockertestを使ったクリーンな単体テスト環境構築方法について紹介しました。お疲れ様でした。