Go+Gin+GORM開発環境で食品管理APIを作る～単体テスト編～

# はじめに
[こちらの記事](https://qiita.com/sedori/items/ce31e841ba904dbc2700)でクリーンアーキテクチャを意識した構成に変更した食品管理APIで単体試験(Unit Test)を実装するための手法をまとめました。

# 単体試験とは

コードの最小機能ユニットをテストするプロセスのことです。

https://aws.amazon.com/jp/what-is/unit-testing/


[前回記事](https://qiita.com/sedori/items/ce31e841ba904dbc2700)の最後にも書きましたが、このシステムでいえば`func`単位で行う試験ということになりますね。

ただし、クリーンアーキテクチャに対応する前のソースコードに対して単体試験を行おうとした場合、試験対象とする処理の中で呼び出される処理まで実行されてしまうため、イマイチという話でした。

:::note info
クリーンアーキテクチャにおける試験は、抽象(インターフェイス)を参照(依存)するため、実装を簡単に差し替えられることができます。
:::

# 検証環境

[前回記事](https://qiita.com/sedori/items/ce31e841ba904dbc2700)参照

# ディレクトリ構成

最終的には次のようになります。
本書ですべての試験を実装するわけではありません。説明用に未完成な状態となっています。

```shell
.
├── .air.toml
├── Dockerfile
├── docker-compose.yml
├── domain
│   └── repository
│       ├── cooking_repository.go
│       └── mock                                  ★本書で追加
│           └── mock_cooking_repository.go        ★本書で追加
├── go.mod
├── go.sum
├── handler
│   ├── api_route.go
│   ├── rest
│   │   └── cooking_handler.go
│   ├── wire.go
│   └── wire_gen.go
├── library
│   └── database
│       └── database.go
├── main.go
├── memo.txt
├── tmp
│   ├── build-errors.log
│   └── main
└── usecase
    ├── cooking_usecase.go
    ├── cooking_usecase_test.go        ★本書で追加
    └── mock                           ★本書で追加(任意)
        └── mock_cooking_usecase.go    ★本書で追加(任意)

10 directories, 20 files
```

# 実行条件

```shell
Golang v1.22.3
Gin v1.10.0
GORM v1.25.10

air v1.52.0
wire v0.6.0

mock v1.6.0
testify v1.9.0

MySQL 8.0.29

```

テストを実装するにあたり、`mock`と`testify`を導入します。

```shell
$ go get github.com/golang/mock/gomock
$ go get github.com/stretchr/testify/assert
```

もし本書に記載している以外のテストを実装する際にほかのライブラリが必要となった場合は、適宜追加してください。


# テストケースの実装

本書ではusecaseのテストケースを1件、実装してみます。

### mockの準備

usecaseではrepositoryを注入しているため、repositoryのmockを作ります。

mockの実装には`github.com/golang/mock/gomock`のmockgenコマンドを利用します。

```shell
$ cd ./domain/repository
$ mockgen -source=cooking_repository.go -destination=./mock/mock_cooking_repository.go
```

このコマンドは`-source`に指定したファイルのmockを`-destination`に作成します。
`./domain/repository/mock`に`mock_cooking_repository.go`が作成されたかと思います。

<details><summary>mock_cooking_repository.goの中身はこちら</summary>

```golang:./domain/repository/mock/mock_cooking_repository.go
// Code generated by MockGen. DO NOT EDIT.
// Source: cooking_repository.go

// Package mock_repository is a generated GoMock package.
package mock_repository

import (
	repository "go_project/domain/repository"
	reflect "reflect"

	gomock "github.com/golang/mock/gomock"
)

// MockCookingRepository is a mock of CookingRepository interface.
type MockCookingRepository struct {
	ctrl     *gomock.Controller
	recorder *MockCookingRepositoryMockRecorder
}

// MockCookingRepositoryMockRecorder is the mock recorder for MockCookingRepository.
type MockCookingRepositoryMockRecorder struct {
	mock *MockCookingRepository
}

// NewMockCookingRepository creates a new mock instance.
func NewMockCookingRepository(ctrl *gomock.Controller) *MockCookingRepository {
	mock := &MockCookingRepository{ctrl: ctrl}
	mock.recorder = &MockCookingRepositoryMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCookingRepository) EXPECT() *MockCookingRepositoryMockRecorder {
	return m.recorder
}

// DeleteFoodRepository mocks base method.
func (m *MockCookingRepository) DeleteFoodRepository(food repository.Food) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "DeleteFoodRepository", food)
	ret0, _ := ret[0].(error)
	return ret0
}

// DeleteFoodRepository indicates an expected call of DeleteFoodRepository.
func (mr *MockCookingRepositoryMockRecorder) DeleteFoodRepository(food interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFoodRepository", reflect.TypeOf((*MockCookingRepository)(nil).DeleteFoodRepository), food)
}

// GetAllTheFoodRepository mocks base method.
func (m *MockCookingRepository) GetAllTheFoodRepository() ([]repository.Food, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "GetAllTheFoodRepository")
	ret0, _ := ret[0].([]repository.Food)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// GetAllTheFoodRepository indicates an expected call of GetAllTheFoodRepository.
func (mr *MockCookingRepositoryMockRecorder) GetAllTheFoodRepository() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTheFoodRepository", reflect.TypeOf((*MockCookingRepository)(nil).GetAllTheFoodRepository))
}

// GetOneFoodRepository mocks base method.
func (m *MockCookingRepository) GetOneFoodRepository(food_name string, preload_flag bool) (repository.Food, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "GetOneFoodRepository", food_name, preload_flag)
	ret0, _ := ret[0].(repository.Food)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// GetOneFoodRepository indicates an expected call of GetOneFoodRepository.
func (mr *MockCookingRepositoryMockRecorder) GetOneFoodRepository(food_name, preload_flag interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOneFoodRepository", reflect.TypeOf((*MockCookingRepository)(nil).GetOneFoodRepository), food_name, preload_flag)
}

// RegisterForFoodRepository mocks base method.
func (m *MockCookingRepository) RegisterForFoodRepository(food_name string, ingredients []string) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "RegisterForFoodRepository", food_name, ingredients)
	ret0, _ := ret[0].(error)
	return ret0
}

// RegisterForFoodRepository indicates an expected call of RegisterForFoodRepository.
func (mr *MockCookingRepositoryMockRecorder) RegisterForFoodRepository(food_name, ingredients interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterForFoodRepository", reflect.TypeOf((*MockCookingRepository)(nil).RegisterForFoodRepository), food_name, ingredients)
}

// UpdateFoodRepository mocks base method.
func (m *MockCookingRepository) UpdateFoodRepository(food repository.Food, ingredients []string) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "UpdateFoodRepository", food, ingredients)
	ret0, _ := ret[0].(error)
	return ret0
}

// UpdateFoodRepository indicates an expected call of UpdateFoodRepository.
func (mr *MockCookingRepositoryMockRecorder) UpdateFoodRepository(food, ingredients interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFoodRepository", reflect.TypeOf((*MockCookingRepository)(nil).UpdateFoodRepository), food, ingredients)
}

```

</details>

内容の詳しい説明は割愛しますが、要するにインターフェイスのモックが自動的に作成されたということです。

:::note info
同様にhandlerのテストケースを作るときにはusecaseのmockが必要になりますので、同じように作成します。

```shell:usecaseのmockを作る場合の作成例
$ cd ./usecase
$ mockgen -source=cooking_usecase.go -destination=./mock/mock_cooking_usecase.go
```

※本書ではhandlerのテストの実装はしません。
:::

### テストケースの実装

mockを使ったテストケースを実装していきます。

Go言語では試験対象となるソースファイルの末尾に`_test`とつけたソースファイルを作成します。

```shell
$ touch cooking_usecase_test.go
```

```golang:./usecase/cooking_usecase_test.go
package usecase_test

import (
    "testing"

    "github.com/golang/mock/gomock"
    "github.com/stretchr/testify/assert"
    "go_project/domain/repository"
    "go_project/domain/repository/mock"
    "go_project/usecase"
)

// 食品全件取得ユースケースのテスト
// 正常系
// 取得結果が期待値と一致することを検証する
func TestGetFoodsUsecase(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := mock_repository.NewMockCookingRepository(ctrl)

    // テストデータ
    foods := []repository.Food{
        {
            Id: 1,
            FoodName: "Pizza",
            Ingredients : []repository.Ingredient{
                {
                    Id : 1,
                    IngredientName : "Cheese",
                },
                {
                    Id : 2,
                    IngredientName : "Tomato",
                },
            },
        },
        {
            Id: 2,
            FoodName: "Burger",
            Ingredients : []repository.Ingredient{
                {
                    Id : 1,
                    IngredientName : "Cheese",
                },
                {
                    Id : 3,
                    IngredientName : "lettuce",
                },
            },
        },
    }

    // 期待値
    expected := []string{"Pizza", "Burger"}

    // モックの振る舞いを定義
    mockRepo.EXPECT().GetAllTheFoodRepository().Return(foods, nil)

    // テスト対象のメソッド
    uc := usecase.NewCookingUsecase(mockRepo)
    result, err := uc.GetFoodsUsecase()

    // 戻り値を検証する
    assert.NoError(t, err)
    assert.Equal(t, expected, result)
}

/* 他のテストケースも実装する */
```

それでは細かく見ていきましょう。

```golang
import (
    "testing"

    "github.com/golang/mock/gomock"
    "github.com/stretchr/testify/assert"
    "go_project/domain/repository"
    "go_project/domain/repository/mock"
    "go_project/usecase"
)
```

テストケースの実装では`testing`をインポートします。それ以外は試験や実装に必要になります。

```golang
func TestGetFoodsUsecase(t *testing.T) {/*処理*/}
```

テストケースの関数名は先頭に`Test`を付けます。また、引数は`t *testing.T`になります。

```golang
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := mock_repository.NewMockCookingRepository(ctrl)
```

`gomock.NewController(t)`で`mock`の利用を宣言します。
試験対象である`GetFoodsUsecase`関数では`CookingRepository`を利用しているため、この試験では先ほど`mockgen`で生成した`NewMockCookingRepository`を利用します。

```golang
    // モックの振る舞いを定義
    mockRepo.EXPECT().GetAllTheFoodRepository().Return(foods, nil)
```

`GetFoodsUsecase`関数が呼び出す`CookingRepository`の`GetAllTheFoodRepository`関数の振る舞いを定義します。

`cooking_repository.go`の`GetAllTheFoodRepository`関数ではDBアクセスして値を返しますが、`mock`を利用することで実際の`GetAllTheFoodRepository`を呼び出すことなく、処理結果を返却することができます。

```
    // テスト対象のメソッド
    uc := usecase.NewCookingUsecase(mockRepo)
    result, err := uc.GetFoodsUsecase()
```

`usecase.NewCookingUsecase(mockRepo)`で`mock`の`repository`を注入して`usecase`を利用できるようにします。
そしてテスト対象である`GetFoodsUsecase`を実行します。

```
    assert.NoError(t, err)
    assert.Equal(t, expected, result)
```

処理結果の検証はこちら。
今回は正常に値が返ることを検証する試験ですので、エラーがないことと取得結果を検証しています。

なお、`repository.GetAllTheFoodRepository`から返却される値は`repository.Food`構造体ですが、`GetFoodsUsecase`ではこれを`[]string`になるよう整形していますので、`GetAllTheFoodRepository`の戻り値に設定した`foods`が`expected`のように整形させて返却されることを期待して、検証しています。

### テストケースの実行

作成したらテストを実行してみます。
テストの実施は`go test テストケースファイル テスト対象ファイル`で行います。

```shell
$ cd ./usecase
$ go test cooking_usecase_test.go cooking_usecase.go
ok      command-line-arguments  0.003s
```

`go test`だけでも実行できますが、ほかにもテストケースがある場合はまとめて実行されます。

:::note info
- テストでエラーがある場合
```shell
$ go test cooking_usecase_test.go cooking_usecase.go
--- FAIL: TestGetFoodsUsecase (0.00s)
    cooking_usecase_test.go:66:
                Error Trace:    /app/usecase/cooking_usecase_test.go:66
                Error:          Not equal:
                                expected: []string{"Pizza", "Burger", "Cheese"}
                                actual  : []string{"Pizza", "Burger"}

                                Diff:
                                --- Expected
                                +++ Actual
                                @@ -1,5 +1,4 @@
                                -([]string) (len=3) {
                                +([]string) (len=2) {
                                  (string) (len=5) "Pizza",
                                - (string) (len=6) "Burger",
                                - (string) (len=6) "Cheese"
                                + (string) (len=6) "Burger"
                                 }
                Test:           TestGetFoodsUsecase
FAIL
FAIL    command-line-arguments  0.003s
FAIL
```

- カバレッジ出力(-coverオプション)
```shell
go test -cover
PASS
coverage: 13.7% of statements
ok      go_project/usecase      0.003s
```

- カバレッジ出力(可視化)
```shell
$ go test -v -cover -coverprofile=出力ファイル名
$ go tool cover -html=出力ファイル名 -o 出力ファイル名.html
```
:::

# まとめ

いかがだったでしょうか。依存性の注入を行ったことでテストがよりシンプルに、疎で行えるようになったのがわかるかと思います。

もし本書をハンズオンで進めていましたら、ほかのテストケースも実装してみましょう。