単体テストを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を使ったクリーンな単体テスト環境構築方法について紹介しました。お疲れ様でした。