GoでRepository層のテストをDockerを使って実装してみた

# はじめに
GoのプロジェクトにおいてRepositoryテストを実装しました。
Repositoryテストをするにあたって、実態のDBとの接続やデータの取扱など、考慮すべき点が多かったため、実装を先送りしておりましたが、Dockerを用いた手法により、実装したので、ご紹介します。

Goバージョンは1.1４
DBはMySQL
となります。

また今回使用した、サンプルコードは以下にありますので合わせてご覧いただけると幸いです。
https://github.com/ppco/go_repository_test_sample

# アプローチ
テストを実装するにあたって、クエリの評価をしっかりと行いたいと共に、しっかりと然るべきところでエラーを返却するか（例えば`Scan()`でエラーになった時など）を確認したかったので、以下２つの観点からアプローチを行いました。

1. 実態DBを使ったクエリの評価
2. Mockを用いたコードの評価

## 使用ライブラリなど
1については上述の通りDockerによるテスト用DBを建てる事を前提としますが、テストデータの準備についてはRails如く`yml`ファイルでデータを投入し、テスト単位で`Rollback`してくれるものがあったのでそちらを利用しています。
https://github.com/go-testfixtures/testfixtures

2についてはdatabase/sqlをmock化してくれる割と有名なものを利用しています。
https://github.com/DATA-DOG/go-sqlmock


# 構成
以下の様な構成になっています。（一部抜粋）

```
root/
　├ db/
　│　├ fixtures/
　│　│　　└sample.yml
　│　└ schema.sql
　├ project/
　│　├ repositories/
　│　│　　├ testutils/
　│　│　　│　　└ test_utils.go
　│　│　　├ sample_repository.go
　│　│　　├ sample_repository_test.go
　│　│　　└ repository_entrypoint_test.go
　│　├ infrastructures/
　│　└ interfaces/
　├ Dockerfile.testing
　└ docker-compose.testing.yml

```

## db
fixtureについては、`id`は直指定しなければならないものの、時刻などSQL関数を実行したい場合は`RAW=`と記載しておけば実行してくれます。

```sample.yml
- id: 1
  code: hoge
  name: ほげ
  created_at: RAW=NOW()
  updated_at: RAW=NOW()
```

schemaについては、手運用は面倒くさいので、`mysqldump`から取得したデータをそのまま格納するようにしています。
（取り込む側で制御）

## Dockerfile.testing / docker-compose.testing.yml
テスト専用としてそれ単体で動くようファイル群を切り出しています。
通常の開発用DBとポートが被らないよう3307で定義しています。
また余談となりますが、筆者のCI/CD環境でも動くようHTTP_PROXYの設定をしています。


```docker-compose.testing.yml
version: '3'
services:
  tester:
    build:
      context: .
      dockerfile: Dockerfile.testing
      args:
        - HTTP_PROXY=${HTTP_PROXY}
    links:
      - db_test
    environment:
      - UNIT_TEST_MYSQL_CONNECTION_STRING=root:password@tcp(db_test:3307)/db_test?parseTime=true&loc=Local
    command: go test ./... -v

  db_test:
    image: mysql:5.7.12
    environment:
      MYSQL_ROOT_PASSWORD: 'password'
      MYSQL_DATABASE: 'db_test'
    ports:
      - "3307:3307"
    command: mysqld --port 3307
```

```Dockerfile
FROM golang:1.14

ARG HTTP_PROXY

WORKDIR /go/src/project

COPY ./project/go.mod ./
COPY ./project/go.sum ./

RUN http_proxy=$HTTP_PROXY go mod download

COPY . ../
```

## repository_entrypoint_test.go
repositoryパッケージテストのエントリーポイントとして、定義します。
後述する`Setup`を呼び出す処理をtestingパッケージの`*testing.M`によって実行させます。
これにより、テスト実行時に必ず初回にSetup処理が走るようになります。

```repository_entrypoint_test.go
func TestMain(m *testing.M) {
	os.Exit(testutils.Setup(m))
}
```

## test_utils.go
repository_entrypoint_test.goから呼び出される`Setup`関数によりDB初期化処理やテーブル作成・fixtureの適用などを行います。
テーブルの作成については、`mysqldump`から取得したschemaをそのまま用いますが、`CREATE`及び`DROP`に限定してテーブルを作成します。

一部抜粋

```test_utils.go
var (
	dbConn   *sqlx.DB
	fixtures *testfixtures.Loader
)
const schemaDirRelativePathFormat = "%s/../../../../db/schema.sql"
const fixturesDirRelativePathFormat = "%s/../../../../db/fixtures"

func Setup(m *testing.M) int {
	shutdown := SetupDBConn()
	defer shutdown()

	createTablesIfNotExist()
	setupFixtures()

	return m.Run()
}

func SetupDBConn() func() {
	db, err := sqlx.Connect("mysql", os.Getenv("UNIT_TEST_MYSQL_CONNECTION_STRING"))
	if err != nil {
		log.Fatalf("Could not connect to mysql: %s", err)
	}

	dbConn = db

	return func() { dbConn.Close() }
}

func createTablesIfNotExist() {
	_, pwd, _, _ := runtime.Caller(0)
	schemaPath := fmt.Sprintf(schemaDirRelativePathFormat, path.Dir(pwd))
	b, err := ioutil.ReadFile(schemaPath)
	if err != nil {
		log.Fatalf("schema reading error: %v", err)
	}

	queries := strings.Split(string(b), ";")

	_, err = dbConn.Exec("SET FOREIGN_KEY_CHECKS=0;")
	if err != nil {
		log.Fatalf("exec schema error: %v", err)
	}

	for _, query := range queries {
		if strings.Contains(query, "CREATE TABLE") || strings.Contains(query, "DROP TABLE") {
			_, err = dbConn.Exec(query)
			if err != nil {
				log.Fatalf("exec schema error: %v, query: %s", err, query)
			}
		}
	}

	_, err = dbConn.Exec("SET FOREIGN_KEY_CHECKS=1;")
	if err != nil {
		log.Fatalf("exec schema error: %v", err)
	}
}

func setupFixtures() {
	_, pwd, _, _ := runtime.Caller(0)

	dir := fmt.Sprintf(fixturesDirRelativePathFormat, path.Dir(pwd))
	fix, err := testfixtures.New(
		testfixtures.Database(dbConn.DB),
		testfixtures.Dialect("mysql"),
		testfixtures.Directory(dir),
	)
	if err != nil {
		log.Fatalf("setup fixtures error: %v", err)
	}
	fixtures = fix
}


```

# 実態DBを使ったクエリ評価テスト
事前準備処理ができたので、実際にテストを定義していきます。
ここでは、特定のテーブルから情報をSELECTしてくるメソッドについてのテストの一例を示します。
`testutils.GetTestDBConn()`関数によって、コネクションを取得します。この時、fixtureの再構築（TRANCATE→INSERT）をするため、テスト実行毎にレコードをリセットすることが可能です。

```sample_repository_test.go
func Test_Sample実態DBテスト(t *testing.T) {
	db := testutils.GetTestDBConn()
	mySQLHandler := &infrastructures.MySQLHandler{}
	mySQLHandler.Conn = db
	repo := SampleRepository{IDbHandler: mySQLHandler}

	expectedSample := &model.Sample{
		ID:   1,
		Code: "hoge",
		Name: "ほげ",
	}
	actualSample, err := repo.FindSample(int64(1))

	if !reflect.DeepEqual(expectedSample, actualSample) {
		t.Errorf("%sで取得結果が異なる expected:%v actual:%v", t.Name(), expectedSample, actualSample)
	}

	if err != nil {
		t.Errorf("%sでerrがnilじゃない err:%v", t.Name(), err)
	}
}

```


# Mockを使ったテスト
Mock化したDBを使ったテストの一例を示します。
DBライブラリにsqlxを使用しているので、生成したsqlmockをsqlxにラップして、DIします。

プロダクトコードにおいて、`Query`を実行した際に、エラーを発生させるので、今回は予めDBを`Close()`しておきます。
（この例だと、Mock化の恩恵はあまりないですが、、）

```sample_repository_test.go
func Test_振る舞いテスト_コネクションエラー時のテスト(t *testing.T) {
	mockDB, _, _ := sqlmock.New()
	defer mockDB.Close()
	sqlxDB := sqlx.NewDb(mockDB, "sqlmock")

	mockHandler := &infrastructures.MySQLHandler{}
	mockHandler.Conn = sqlxDB

	expectedErr := errors.New("sql: database is closed")

	sqlxDB.Close()
	sampleRepository := SampleRepository{IDbHandler: mockHandler}

	_, actualErr := sampleRepository.FindSample(int64(1))
	if !reflect.DeepEqual(expectedErr.Error(), actualErr.Error()) {
		t.Errorf("%sで期待結果相違 expectedErr:%v actualErr:%v", t.Name(), expectedErr, actualErr)
	}
}
```
その他の例に関しては[サンプルコード](https://github.com/ppco/go_repository_test_sample)を参照していただけると幸いです。

また、sqlmockの詳しい使い方に関しては日本語記事だと[Goでデータベースを簡単にモック化する【sqlmock】](https://qiita.com/gold-kou/items/cb174690397f651e2d7f)がわかりやすかったです。

# まとめ
DIによって、Repository以外のテスト（Service層やApplication層など）については実装しておりましたが、今回Repositoryにも導入することができたため、良かったと思います。実装してみて、結果を確認するためのクエリなども逐一記述しなければいけないため、今後はORMの導入なども視野にいれていければと思います。

また、余談となりますが、こちらのテストを用いたCI/CD環境の構築編についても記事にしたので、環境が一致する方はなにか参考になれば幸いです。

https://qiita.com/ppco/items/57a28c5e4fe28226dda8


# 参考
こちらの記事をだいぶ参考にさせていただきました！ありがとうございます！
GoのAPIのテストにおける共通処理
https://medium.com/@timakin/go-api-testing-173b97fb23ec

Goでデータベースを簡単にモック化する【sqlmock】
https://qiita.com/gold-kou/items/cb174690397f651e2d7f