【備忘録】テストコード作成時のキモチ

# テストコードを書くということ

**「テストコードを作成する」** = **「テスト対象コードの正しい動作を検証するため、仮データやモックを用いて実行する検証プログラムを作成すること」**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868342/b5781c68-c790-4b0d-b3d0-055970f395b3.png)


# テスト対象
**usecase/create_account.go**
```go
package usecase

import (
	"context"
	"time"

	"github.com/gsabadini/go-clean-architecture/domain"
)

type (
	// CreateAccountUseCase defines the input port
	CreateAccountUseCase interface {
		Execute(context.Context, CreateAccountInput) (CreateAccountOutput, error)
	}

	// CreateAccountInput represents the input data
	CreateAccountInput struct {
		Name    string `json:"name" validate:"required"`
		CPF     string `json:"cpf" validate:"required"`
		Balance int64  `json:"balance" validate:"gt=0,required"`
	}

	// CreateAccountPresenter defines the output port
	CreateAccountPresenter interface {
		Output(domain.Account) CreateAccountOutput
	}

	// CreateAccountOutput represents the output data
	CreateAccountOutput struct {
		ID        string  `json:"id"`
		Name      string  `json:"name"`
		CPF       string  `json:"cpf"`
		Balance   float64 `json:"balance"`
		CreatedAt string  `json:"created_at"`
	}

	createAccountInteractor struct {
		repo       domain.AccountRepository
		presenter  CreateAccountPresenter
		ctxTimeout time.Duration
	}
)

// NewCreateAccountInteractor initializes a new instance of createAccountInteractor
func NewCreateAccountInteractor(
	repo domain.AccountRepository,
	presenter CreateAccountPresenter,
	t time.Duration,
) CreateAccountUseCase {
	return createAccountInteractor{
		repo:       repo,
		presenter:  presenter,
		ctxTimeout: t,
	}
}

// Execute executes the use case logic
func (a createAccountInteractor) Execute(ctx context.Context, input CreateAccountInput) (CreateAccountOutput, error) {
	ctx, cancel := context.WithTimeout(ctx, a.ctxTimeout)
	defer cancel()

	account := domain.NewAccount(
		domain.AccountID(domain.NewUUID()),
		input.Name,
		input.CPF,
		domain.Money(input.Balance),
		time.Now(),
	)

	account, err := a.repo.Create(ctx, account)
	if err != nil {
		return a.presenter.Output(domain.Account{}), err
	}

	return a.presenter.Output(account), nil
}
```

---

# **テスト作成手順**

## 1. **テスト対象ファイルの決定**
テスト対象は `usecase/create_account.go`。

## 2. **テストコードファイルの作成**
テスト対象のファイル名に `_test` を追加し、`usecase/create_account_test.go` を作成。

## 3. **テスト対象メソッドの決定**
`createAccountInteractor` 構造体の `Execute` メソッドをテスト対象とする。

## 4. **テストメソッドの定義**
Go のテストメソッドは `Test` プレフィックスを付けるため、以下のように命名。

**usecase/create_account_test.go**
```go
package usecase

import "testing"

func TestCreateAccountInteractor_Execute(t *testing.T) {
    // テストコードを記述
}
```

## 5. **テストに必要な要素の特定**

`createAccountInteractor` 構造体の `Execute` メソッドをテストするには、まず `createAccountInteractor` オブジェクトを生成する必要がある。そのために `NewCreateAccountInteractor` メソッドを実行する。

### **5.1 `NewCreateAccountInteractor` の引数**
以下のメソッドから、オブジェクトの生成に必要な要素を確認する。

```go
// NewCreateAccountInteractor initializes a new createAccountInteractor instance
func NewCreateAccountInteractor(
	repo domain.AccountRepository,
	presenter CreateAccountPresenter,
	t time.Duration,
) CreateAccountUseCase {
	return createAccountInteractor{
		repo:       repo,
		presenter:  presenter,
		ctxTimeout: t,
	}
}
```

このメソッドの引数から、以下の 3 つの要素が必要になる。
- `repo` (`domain.AccountRepository`)
- `presenter` (`CreateAccountPresenter`)
- `t` (`time.Duration`)

### **5.2 `Execute` メソッドの引数**
`createAccountInteractor` オブジェクトを生成した後、テスト対象である `Execute` メソッドを実行する。

```go
// Execute executes the use case logic
func (a createAccountInteractor) Execute(ctx context.Context, input CreateAccountInput) (CreateAccountOutput, error) {
	// (省略)
}
```

このメソッドの引数から、以下の 2 つの要素が必要になる。
- `ctx` (`context.Context`)
- `input` (`CreateAccountInput`)

テストを実行するために必要な要素は、以下の 5 つ。

| 項目       | 型                          | 用途 |
|------------|----------------------------|----------------------|
| `repo`     | `domain.AccountRepository`  | データの保存・取得   |
| `presenter` | `CreateAccountPresenter`   | 出力データの整形     |
| `t`        | `time.Duration`             | コンテキストのタイムアウト設定 |
| `ctx`      | `context.Context`           | 実行時のコンテキスト管理 |
| `input`    | `CreateAccountInput`        | 入力データ |

この 5 つの要素を準備すれば、`createAccountInteractor` の `Execute` メソッドをテストできる。

## 6. **必要な要素を用意する**
特定した必要要素を用意する。


### **6.1 `domain.AccountRepository` 型 `repo` の用意**

`createAccountInteractor` の `Execute` メソッドをテストするために、`domain.AccountRepository` をモック化する。以下の手順で `repo` を準備する。

---
#### **6.1.1 型の定義**
テストケースの構造体で、リポジトリの型を定義する。

```go
repository domain.AccountRepository // リポジトリの型を定義
```
---
#### **6.1.2 モックリポジトリの作成**
実際の `domain.AccountRepository` の代わりとなる、テスト用のモッククラス `mockAccountRepoStore` を定義する。

```go
type mockAccountRepoStore struct {
	domain.AccountRepository

	result domain.Account
	err    error
}
```
**なぜ `mockAccountRepository` ではなく `mockAccountRepoStore` なのか？**  
`mockAccountRepoStore` という名前にすることで、**データの入出力は `Repository` の動作と同じだが、厳密にはリポジトリの働きをしていない** ことを明示している。

---
#### **6.1.3 `Create` メソッドのモック化**
`Execute` メソッド内で `repo.Create(ctx, account)` が呼ばれるため、`Create` メソッドを擬似的に再現する必要がある。

```go
func (m mockAccountRepoStore) Create(_ context.Context, _ domain.Account) (domain.Account, error) {
	return m.result, m.err
}
```
このメソッドにより、テスト時に `Create` メソッドが呼ばれると、事前に設定した `result`（ダミーの `Account` オブジェクト）と `err` を返すようになる。

---
#### **6.1.4 実際のリポジトリインスタンスを生成**
作成した `mockAccountRepoStore` を利用し、テストケースで使用するリポジトリを作成する。

```go
{
	repository: mockAccountRepoStore{
		result: domain.NewAccount(
			"3c096a40-ccba-4b58-93ed-57379ab04680",
			"Test",
			"02815517078",
			19944,
			time.Time{},
		),
		err: nil,
	}
}
```
この設定により、`repository.Create()` が呼ばれると、固定の `Account` データが返される。

---
現状のテストコード
```go
package usecase

import (
	"context"
	"testing"
	"time"

	"github.com/gsabadini/go-clean-architecture/domain"
)



// 2. リポジトリの代わりとなるモッククラスを定義
type mockAccountRepoStore struct {
	domain.AccountRepository

	result domain.Account
	err    error
}

// 3. Create メソッドのモック化
func (m mockAccountRepoStore) Create(_ context.Context, _ domain.Account) (domain.Account, error) {
	return m.result, m.err
}

func TestCreateAccountInteractor_Execute(t *testing.T) {
	tests := []struct {
		repository domain.AccountRepository // 1. 型の定義
	}{
		// 4. モックリポジトリのインスタンスを作成
		{
			repository: mockAccountRepoStore{
				result: domain.NewAccount(
					"3c096a40-ccba-4b58-93ed-57379ab04680",
					"Test",
					"02815517078",
					19944,
					time.Time{},
				),
				err: nil,
			},
		},
	}
}
```

### **6.2 `CreateAccountPresenter`型 `presenter`の用意**

`createAccountInteractor` の `Execute` メソッドをテストするために、`CreateAccountPresenter` をモック化する。以下の手順で `presenter` を準備する。

---
#### **6.2.1 型の定義**
テストケースの構造体で、プレゼンターの型を定義する。

```go
presenter CreateAccountPresenter // プレゼンターの型を定義
```

---
#### **6.2.2 モックプレゼンターの作成**
実際の `CreateAccountPresenter` の代わりとなる、テスト用のモッククラス `mockCreateAccountPresenter` を定義する。

```go
type mockCreateAccountPresenter struct {
	result CreateAccountOutput
}
```

**なぜ `mockCreateAccountPresenter` を作成するのか？**  
`mockCreateAccountPresenter` を作成することで、`Execute` メソッドの `presenter.Output(account)` が呼ばれた際に、事前に定義した `result`（ダミーの `CreateAccountOutput` オブジェクト）を返すことができる。

---
#### **6.2.3 `Output` メソッドのモック化**
`Execute` メソッド内で `presenter.Output(account)` が呼ばれるため、`Output` メソッドを擬似的に再現する必要がある。

```go
func (m mockCreateAccountPresenter) Output(_ domain.Account) CreateAccountOutput {
	return m.result
}
```

このメソッドにより、テスト時に `Output` メソッドが呼ばれると、事前に設定した `result`（ダミーの `CreateAccountOutput` オブジェクト）を返すようになる。

---
#### **6.2.4 実際のプレゼンターインスタンスを生成**
作成した `mockCreateAccountPresenter` を利用し、テストケースで使用するプレゼンターを作成する。

```go
{
	presenter: mockCreateAccountPresenter{
		result: CreateAccountOutput{
			ID:        "3c096a40-ccba-4b58-93ed-57379ab04680",
			Name:      "Test",
			CPF:       "02815517078",
			Balance:   199.44,
			CreatedAt: time.Time{}.String(),
		},
	},
}
```

この設定により、`presenter.Output()` が呼ばれると、固定の `CreateAccountOutput` データが返される。

---
現状のテストコード
```go
package usecase

import (
	"context"
	"testing"
	"time"

	"github.com/gsabadini/go-clean-architecture/domain"
)

type mockAccountRepoStore struct {
	domain.AccountRepository

	result domain.Account
	err    error
}

func (m mockAccountRepoStore) Create(_ context.Context, _ domain.Account) (domain.Account, error) {
	return m.result, m.err
}

// 2. プレゼンターの代わりとなるモッククラスを定義
type mockCreateAccountPresenter struct {
	result CreateAccountOutput
}

// 3. Output メソッドのモック化
func (m mockCreateAccountPresenter) Output(_ domain.Account) CreateAccountOutput {
	return m.result
}

func TestCreateAccountInteractor_Execute(t *testing.T) {
	tests := []struct {
        repository domain.AccountRepository
		presenter CreateAccountPresenter // 1. 型の定義
	}{
		// 4. モックプレゼンターのインスタンスを作成
		{
            repository: mockAccountRepoStore{
				result: domain.NewAccount(
					"3c096a40-ccba-4b58-93ed-57379ab04680",
					"Test",
					"02815517078",
					19944,
					time.Time{},
				),
				err: nil,
			},
			presenter: mockCreateAccountPresenter{
				result: CreateAccountOutput{
					ID:        "3c096a40-ccba-4b58-93ed-57379ab04680",
					Name:      "Test",
					CPF:       "02815517078",
					Balance:   199.44,
					CreatedAt: time.Time{}.String(),
				},
			},
		},
	}
}
```

### **6.3 `time.Duration`型 `t`の用意**
`time`パッケージのものを使うので、改めて用意する必要はない。

### **6.4 `context.Context`型 `ctx`の用意**
`context`パッケージのものを使うので、改めて用意する必要はない。

### **6.5 `CreateAccountInput`型 `input`の用意**

`createAccountInteractor` の `Execute` メソッドをテストするために、`CreateAccountInput` を準備する。以下の手順で `input` を定義する。

---

#### **6.5.1 型の定義**  
テストケースの構造体で、`args` 型の `args` を定義する。

```go
args args // 1. 入力データの型を定義
```

**`args`（arguments）の正式名称**
`args` は **"arguments"（引数）** の略で、関数に渡す引数をまとめるための構造体。

**メリット**
1. **可読性向上**  
   - テストケースごとに引数を整理し、統一感を持たせる。  
   - フィールドが増えても見やすい構造を維持できる。

2. **引数の追加・変更が容易**  
   - `args` にまとめることで、引数が増えても最小限の修正で対応可能。  

---

#### **6.5.2 `CreateAccountInput` の作成**  
`Execute` メソッドのテストを行うため、入力データのモックを作成する。  
`CreateAccountInput` は `Name`、`CPF`、`Balance` の 3 つのフィールドを持っているため、テストケースで適切な値を設定する。

```go
{
    args: args{
        input: CreateAccountInput{
            Name:    "Test",
            CPF:     "02815517078",
            Balance: 19944,
        },
    },
}

```
この設定により、テスト時に `Execute` メソッドへ渡される `input` が適切な値を持つようになる。

---



現状のテストコード
```go
package usecase

import (
	"context"
	"testing"
	"time"

	"github.com/gsabadini/go-clean-architecture/domain"
)

type mockAccountRepoStore struct {
	domain.AccountRepository

	result domain.Account
	err    error
}

func (m mockAccountRepoStore) Create(_ context.Context, _ domain.Account) (domain.Account, error) {
	return m.result, m.err
}

type mockCreateAccountPresenter struct {
	result CreateAccountOutput
}

func (m mockCreateAccountPresenter) Output(_ domain.Account) CreateAccountOutput {
	return m.result
}

func TestCreateAccountInteractor_Execute(t *testing.T) {
	tests := []struct {
        args args //1. 入力データの型を定義
        repository domain.AccountRepository
		presenter CreateAccountPresenter
	}{
		{
            //2. CreateAccountInputの作成
            args: args{
				input: CreateAccountInput{
					Name:    "Test",
					CPF:     "02815517078",
					Balance: 19944,
				},
			},
            repository: mockAccountRepoStore{
				result: domain.NewAccount(
					"3c096a40-ccba-4b58-93ed-57379ab04680",
					"Test",
					"02815517078",
					19944,
					time.Time{},
				),
				err: nil,
			},
			presenter: mockCreateAccountPresenter{
				result: CreateAccountOutput{
					ID:        "3c096a40-ccba-4b58-93ed-57379ab04680",
					Name:      "Test",
					CPF:       "02815517078",
					Balance:   199.44,
					CreatedAt: time.Time{}.String(),
				},
			},
		},
	}
}
```
## 7. **テストを実行する処理を追加**

ここまでで `createAccountInteractor` の `Execute` メソッドをテストする準備が整った。  
次に、テストケースをループで実行し、期待通りの動作をしているかを検証する。

---

### **7.1 テストケースのループ処理**
定義した `tests` スライスの各テストケースを順番に実行する。

```go
for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        var uc = NewCreateAccountInteractor(tt.repository, tt.presenter, time.Second)

        result, err := uc.Execute(context.TODO(), tt.args.input)

        if (err != nil) && (err.Error() != tt.expectedError) {
            t.Errorf("[TestCase '%s'] Result: '%v' | ExpectedError: '%v'", tt.name, err, tt.expectedError)
        }

        if !reflect.DeepEqual(result, tt.expected) {
            t.Errorf("[TestCase '%s'] Result: '%v' | Expected: '%v'", tt.name, result, tt.expected)
        }
    })
}
```
以下で細かく解説。

---

### **7.2 `t.Run` で各テストケースを独立実行**
`testing.T` の `Run` メソッドを使い、各テストケースをサブテストとして管理する。  
これにより、どのテストケースが失敗したかを明確に特定できる。

---

### **7.3 `NewCreateAccountInteractor` のインスタンスを生成**
各テストケースごとに `CreateAccountInteractor` の新しいインスタンスを作成する。

```go
var uc = NewCreateAccountInteractor(tt.repository, tt.presenter, time.Second)
```
- `tt.repository`: モックリポジトリを注入  
- `tt.presenter`: モックプレゼンターを注入  
- `time.Second`: タイムアウト設定  

---

### **7.4 `Execute` メソッドを実行**
作成した `uc` インスタンスで `Execute` メソッドを実行し、結果 (`result`) とエラー (`err`) を取得する。

```go
result, err := uc.Execute(context.TODO(), tt.args.input)
```
- `context.TODO()` はテスト環境で `context.Context` を渡すために使用  

---

### **7.5 エラーの検証**
```go
if (err != nil) && (err.Error() != tt.expectedError) {
    t.Errorf("[TestCase '%s'] Result: '%v' | ExpectedError: '%v'", tt.name, err, tt.expectedError)
}
```
- `err` が発生した場合、期待されるエラーメッセージ (`tt.expectedError`) と比較する。  
- `tt.expectedError` はテストケース内で設定されており、例えば以下のように指定される。

```go
expectedError: "error",
```

- 一致しない場合は `t.Errorf` でテスト失敗として記録。

---

### **7.6 期待される出力との比較**
```go
if !reflect.DeepEqual(result, tt.expected) {
    t.Errorf("[TestCase '%s'] Result: '%v' | Expected: '%v'", tt.name, result, tt.expected)
}
```
- `reflect.DeepEqual` を使って、`Execute` メソッドの戻り値 (`result`) が期待値 (`tt.expected`) と一致するかをチェック。  
- `tt.expected` はテストケース内で設定されており、例えば以下のように指定される。

```go
expected: CreateAccountOutput{
    ID:        "3c096a40-ccba-4b58-93ed-57379ab04680",
    Name:      "Test",
    CPF:       "02815517078",
    Balance:   199.44,
    CreatedAt: time.Time{}.String(),
},
```

- 期待値と `Execute` の戻り値 (`result`) が異なる場合、`t.Errorf` でエラーメッセージを出力する。

---

### **7.7 テストの並列実行**
`TestCreateAccountInteractor_Execute` の最初で `t.Parallel()` を指定しているため、各サブテスト (`t.Run`) は並列実行される。  
これにより、テストの実行時間が短縮される。


# 完成したテストコード

テストケースが２つ増えていることに注意。
**usecase/create_account_test.go**
```go
package usecase

import (
	"context"
	"errors"
	"reflect"
	"testing"
	"time"

	"github.com/gsabadini/go-clean-architecture/domain"
)

type mockAccountRepoStore struct {
	domain.AccountRepository

	result domain.Account
	err    error
}

func (m mockAccountRepoStore) Create(_ context.Context, _ domain.Account) (domain.Account, error) {
	return m.result, m.err
}

type mockCreateAccountPresenter struct {
	result CreateAccountOutput
}

func (m mockCreateAccountPresenter) Output(_ domain.Account) CreateAccountOutput {
	return m.result
}

func TestCreateAccountInteractor_Execute(t *testing.T) {
	t.Parallel()

	type args struct {
		input CreateAccountInput
	}

	tests := []struct {
		name          string
		args          args
		repository    domain.AccountRepository
		presenter     CreateAccountPresenter
		expected      CreateAccountOutput
		expectedError interface{}
	}{
		{
			name: "Create account successful",// テストケースの名称
			args: args{
				input: CreateAccountInput{
					Name:    "Test",
					CPF:     "02815517078",
					Balance: 19944,
				},
			},
			repository: mockAccountRepoStore{
				result: domain.NewAccount(
					"3c096a40-ccba-4b58-93ed-57379ab04680",
					"Test",
					"02815517078",
					19944,
					time.Time{},
				),
				err: nil,
			},
			presenter: mockCreateAccountPresenter{
				result: CreateAccountOutput{
					ID:        "3c096a40-ccba-4b58-93ed-57379ab04680",
					Name:      "Test",
					CPF:       "02815517078",
					Balance:   199.44,
					CreatedAt: time.Time{}.String(),
				},
			},
            // 7.6 期待される出力として使用されるデータ
			expected: CreateAccountOutput{
				ID:        "3c096a40-ccba-4b58-93ed-57379ab04680",
				Name:      "Test",
				CPF:       "02815517078",
				Balance:   199.44,
				CreatedAt: time.Time{}.String(),
			},
		},
		{
			name: "Create account successful",
			args: args{
				input: CreateAccountInput{
					Name:    "Test",
					CPF:     "02815517078",
					Balance: 2350,
				},
			},
			repository: mockAccountRepoStore{
				result: domain.NewAccount(
					"3c096a40-ccba-4b58-93ed-57379ab04680",
					"Test",
					"02815517078",
					2350,
					time.Time{},
				),
				err: nil,
			},
			presenter: mockCreateAccountPresenter{
				result: CreateAccountOutput{
					ID:        "3c096a40-ccba-4b58-93ed-57379ab04680",
					Name:      "Test",
					CPF:       "02815517078",
					Balance:   23.5,
					CreatedAt: time.Time{}.String(),
				},
			},
			expected: CreateAccountOutput{
				ID:        "3c096a40-ccba-4b58-93ed-57379ab04680",
				Name:      "Test",
				CPF:       "02815517078",
				Balance:   23.5,
				CreatedAt: time.Time{}.String(),
			},
		},
		{
			name: "Create account generic error",
			args: args{
				input: CreateAccountInput{
					Name:    "",
					CPF:     "",
					Balance: 0,
				},
			},
			repository: mockAccountRepoStore{
				result: domain.Account{},
				err:    errors.New("error"),
			},
			presenter: mockCreateAccountPresenter{
				result: CreateAccountOutput{},
			},
            // 7.5 エラーの検証で使用される期待されるエラー
			expectedError: "error",
			expected:      CreateAccountOutput{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var uc = NewCreateAccountInteractor(tt.repository, tt.presenter, time.Second)

			result, err := uc.Execute(context.TODO(), tt.args.input)

            // 7.6 期待される出力との比較
			if (err != nil) && (err.Error() != tt.expectedError) {
				t.Errorf("[TestCase '%s'] Result: '%v' | ExpectedError: '%v'", tt.name, err, tt.expectedError)
			}

            // 7.5 エラーの検証
			if !reflect.DeepEqual(result, tt.expected) {
				t.Errorf("[TestCase '%s'] Result: '%v' | Expected: '%v'", tt.name, result, tt.expected)
			}
		})
	}
}
```