見出し画像

【Go初学】テスト処理の共通化

今回はAPIのテストについて。最初にテストを書こうとした際、どうにも重複したコードを書いている気がして、上手くまとめられないものかと処理を共通化してみた。何を利用してどのようにテストを書くか、またテスト処理を共通化したかなどを記述したい。

▼利用ライブラリ

テストを記述するにあたっての利用ライブラリは、標準ライブラリの testing をそのまま使っている。理由としては実際の現場でも testing を使っているというのを見るのと、ラップして使い易くしたサードパーティライブラリに慣れる前に標準ライブラリを使っていった方が学べることが多いかと思ったため。また最低限の機能を自分で思いつきで拡張していくのも面白い。

▼ファイル構成

前回の記事などで記載した「パッケージ管理型API」の通り、各APIパッケージは internal 直下に配置し、リソース毎に別パッケージとしている。

internal
├── api
│   └── v1
│       └── user
│           ...
│       └── zo
│           ├── controller.go
│           ├── go.mod
│           ├── go.sum
│           ├── model.go
│           ├── repository_test.go
│           ├── repository.go
│           ├── route_test.go
│           ├── route.go
│           ├── seed_test.go
│           ├── service_test.go
│           ├── service.go
│           └── viewmodel.go
├── config
│   ...
├── middleware
│   ...
├── platform
│   ...
└── test
    ├── config.yml
    ├── go.mod
    ├── go.sum
    ├── test.go
    └── util.go

テストファイルは、アプリケーション実行時に利用するクラスに対応した形で「〜_test.go」ファイルを作成し、パッケージ内でテストを行っている。永続化データまわりのテストであれば「repository_test.go」、ビジネスロジック周りであれば「service_test.go」、リクエストとレスポンス処理周りであれば「route_test.go」としている。
最初は別ディレクトリに分けたかったが、ディレクトリ名とパッケージ名を合わせるルールがあるのと、テスト実行時オプションでカバレッジが確認できなくなるため諦めた(「go test -v -cover」)。

テスト共通処理周りは、外側の各種APIパッケージから利用できるようinternal/test へ抽出した。

▼internal/test について

上記記載の通り、各種APIパッケージのテストで共通して行うような処理を internal/test へ抽出した。こちらで行うことは以下の処理になる。
 ・テスト実行前の初期化
 ・テスト実行前処理
 ・テスト実行
 ・テスト実行後処理

各パッケージのテストのエントリー関数から test.Run() を呼び出して上記の処理を実行する。

func Run(
	t *testing.T,
	tests map[string]func(t *testing.T),  // テスト処理
	before func(),
	after func(),
	seed func(context.Context)) {    // テストデータ準備処理

	teardown := testInit(t)          // テスト実行前の初期化
	t.Cleanup(teardown)

	for name, test := range tests {
		beforeAll(before, seed)  // テスト実行前処理

		t.Run(name, test)        // テスト実行

		afterAll(before)         // テスト実行後処理
	}
}

■テスト実行前の初期化

初期化処理ではアプリケーション起動時と同様、設定ファイルの読み込みやDB接続を行なっている。DBに依存しない形でテストするやり方もあると思うが、今のところデメリットはそれほど感じておらず、SQLの動作確認もできるのでこの形で行なっている。
ここで読み込む設定ファイルは test 直下に配置したもので、テスト用の設定ファイルとなっており、DB接続情報もテスト用DBのものになっている。

func testInit(t *testing.T) func() {
	// 設定ファイルの取得
	config, err := LoadConfig()
	if err != nil {
		t.Fatalf(err.Error())
	}

	err = mydb.Init(config)
	if err != nil {
		mydb.Finalize()
		t.Fatalf(err.Error())
	}

	return mydb.Finalize
}

■テスト実行前処理

実行前処理では実行前処理関数 before 及びテスト用データ生成関数 seed を受け取っており、テスト実行前にこれらを実行する。先にテスト用DBの各テーブルのレコードを空にし、その後 test.Run() に渡された seed 関数を実行し、テスト実行前に必要なテストデータを生成する(repository は使わず、sql.DB.ExecContext() で直接SQLを実行する)。
この形は Rails を参考にしており、テスト実行前は常に状態がリセットされている。DB繋いでテストしたいなと思った時に Rails のシステムを思い出してこんな感じかなと実装してみた。

func beforeAll(before func(), seed func(context.Context)) {
	ctx := context.Background()
	truncateAll(ctx)
	if seed != nil {
		seed(ctx)
	}

	if before != nil {
		before()
	}
}

■テスト実行

テスト実行関数は、test.Run() に渡す際に下記のように幾つかまとめた map を渡している。

var route_tests = map[string]func(t *testing.T){
	"test_route_getall": test_route_getall,
	"test_route_get":    test_route_get,
	"test_route_post":   test_route_post,
	"test_route_update": test_route_update,
	"test_route_delete": test_route_delete}

func test_route_getall(t *testing.T) {
  ...
}

これらを test.Run() の中で、通常 testing を実行する時と同様
testing.T.Run(name, func) でテスト関数を実行する。

■テスト実行後処理

最後にテスト実行後に何か行いたい処理を実行する。特に使っていないが何らか後片付けやログを出力するなどだろうか。

func afterAll(after func()) {
	if after != nil {
		after()
	}
}

あとがき

これら独自の機構は大抵「正しくない」かもしれないが、試行錯誤してそれっぽく作るのは面白いし、手を動かして学びも多いと思う。今回では、パッケージ循環やDBでテストするにはどうするか、テスト用データをどう用意するか、共通処理をどうまとめるかなど。効率悪ければ自然と自分からフィードバックを得られるのでとりあえずやってみるのも非エッセンシャル思考ではあるがよいと思う。

この記事が気に入ったらサポートをしてみませんか?