🔒

dockertest のススメ

2022/10/05に公開

追記: 2024-07-20

Testcontainers を使いましょう。

概要

dockertest は go でテストを書く際に docker 経由で指定したコンテナを起動してくれてテストが終わったらコンテナを削除してくれる便利ライブラリです。

モチベーション

時雨堂では TimescaleDB という PostgreSQL に TSDB 拡張を追加した少し変わった RDBMS を利用しています。
TimescaleDB 専用の関数があったりするため、モックなどを使わずにテストを書くのが現実的です。

dockertest

ory/dockertest: Write better integration tests! Dockertest helps you boot up ephermal docker images for your Go tests with minimal work.

基本的には dockertest の README に書いてある内容を整理した程度です。
実際に使っているコードを公開用に修正したものを共有します。

var q *db.Queries

func TestMain(m *testing.M) {
	pool, err := dockertest.NewPool("")
	// DB の初期化に失敗したときに早く気付きたいの短めに設定
	pool.MaxWait = 10 * time.Second
	if err != nil {
		log.Fatalf("Could not connect to docker: %s", err)
	}

	pwd, _ := os.Getwd()

	runOptions := &dockertest.RunOptions{
		Repository: "timescale/timescaledb",
		// latest だと本番とマッチしなくなる場合があるのでバージョン指定
		Tag: "2.8.0-pg14",
		// ポート番号は固定せずに 0 で listen する
		Env: []string{
			"POSTGRES_USER=postgres",
			"POSTGRES_PASSWORD=password",
			"POSTGRES_DB=defaultdb",
			"listen_addresses='*'",
		},
		// ここでデータベースの初期化ファイルを渡す
		Mounts: []string{
			pwd + "/db/schema.sql:/docker-entrypoint-initdb.d/schema.sql",
		},
	}

	resource, err := pool.RunWithOptions(runOptions,
		func(config *docker.HostConfig) {
			// 処理が終了したらインスタンスを削除する
			config.AutoRemove = true
			config.RestartPolicy = docker.RestartPolicy{
				Name: "no",
			}
		},
	)
	if err != nil {
		log.Fatalf("Could not start resource: %s", err)
	}

	hostAndPort := resource.GetHostPort("5432/tcp")
	databaseUrl := fmt.Sprintf("postgres://postgres:password@%s/defaultdb?sslmode=disable", hostAndPort)
	
	// docker が起動するまで少し時間がかかるのでリトライする
	if err := pool.Retry(func() error {
		config, err := pgxpool.ParseConfig(databaseUrl)
		if err != nil {
			return err
		}
		// pgx の pool 機能を利用してる
		p, err := pgxpool.ConnectConfig(context.Background(), config)
		if err != nil {
			return err
		}
		// 一応 Ping 飛ばして動作確認をする
		if err := p.Ping(context.Background()); err != nil {
			return err
		}
		// sqlc の query を生成
		q = db.New(p)
		return nil
	}); err != nil {
		log.Fatalf("Could not connect to database: %s", err)
	}

	code := m.Run()

	if err := pool.Purge(resource); err != nil {
		log.Fatalf("Could not purge resource: %s", err)
	}

	os.Exit(code)
}

func TestGetSpam(t *testing.T) {
	c := context.Background()

	spamID, err := uuid.NewRandom()
	assert.NoError(t, err)

	spamName := "spam!spam!spam!"

	spam, err := q.CreateSpam(c, db.CreateSpam{
		ID:   spamID,
		Name: spamName,
	})
	assert.NoError(t, err)
	assert.Equal(t, spamID, spam.ID)
	assert.Equal(t, spamName, spam.Name)
}

本当に気軽に使えるので良さそうと思った方は是非使ってみてください。

Discussion