Go言語のテストコードのあれこれ

テストコードを書くためのメモ。随時追加予定。

# 使用するツール
- testing（標準パッケージ）
- testify
- gomock

# testingパッケージ
Go言語のテストは testingパッケージ を利用します。標準ライブラリの一部で、ユニットテストやベンチマークテストをサポートする機能を提供しています。

https://pkg.go.dev/testing

# テストファイルの命名
Goのテストファイルの命名規則は、テスト対象のファイル名に_testをつけます。例えば、`example.go`のテストコードは`example_test.go`という名前にします。

```bash
├── example.go
└── exxample_test.go
```

# テスト関数の命名
- テスト関数は Testから始まる名前をつけ、関数名をその後につける
- テスト関数の引数は *testing.T にする

```go
// example.go
func Add(a, b int) int {
	return a + b
}

// example_test.go
func TestAdd(t *testing.T) {
	if Add(1, 2) != 3 {
		t.Fail()
	}
}
```

# テーブル駆動テスト
1つの関数に対して複数のテストケースを作成する場合、構造体のスライスを作成し、それぞれのテストケースを表す構造体を含めるのが一般的。複数のテストケースを定義し、それらを一つのテスト関数内でループして実行することが可能。

```go
func TestAdd(t *testing.T) {
	// テストケースを定義
	testCases := []struct {
		name     string
		a, b     int
		expected int
	}{
		{"two plus three", 2, 3, 5},
		{"negative and positive", -1, 1, 0},
		{"two negatives", -1, -1, -2},
	}

	// for文で各テストケースを実行
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			result := Add(tc.a, tc.b)
			if result != tc.expected {
				t.Errorf("Add(%d, %d): expected %d, got %d", tc.a, tc.b, tc.expected, result)
			}
		})
	}
}

```

# テストの実行方法
```bash
go test ./...
```
オプション
「-v」は詳細も表示
「--short」はフラグのあるテストファイルをskip
「-cover」はカバレッジの取得

# アサート関数
アサート関数とは、ある式・値が想定したものになっているかを確認する関数です。Goの標準テストパッケージはアサート関数を提供していませんが、if文とtesting.Tの Errorfや Fatalメソッドを使ってアサーションを行うことができます。

```go
func TestAdd(t *testing.T) {
	result := Add(2, 2)
	if result != 4 {
		t.Errorf("Add(2, 2) = %d; want 4", result)
	}
}
```

testify のassert を使えばスッキリ書ける
```go
func TestAdd(t *testing.T) {
	result := Add(2, 2)
	assert.Equal(t, 4, result, "Add(2, 2) = %d; want 4", result)
}
```


# スイート
テストスイートはテストの目的や条件が似ている複数のテストケースを一括りにしたもの。testify の suite 機能。

## セットアップとティアーダウン
セットアップ（前処理）やティアダウン（後処理）を定義することができます。また、各テストケースのセットアップやティアダウンを行うには、ヘルパー関数を作成し、それを各テスト関数内で呼び出すことが一般的です。
さっきのsuite機能と組み合わせて下記のような感じで作れる。

```go

// FooSuite は一連のテストメソッドを含むテストスイートを定義します
type FooSuite struct {
    suite.Suite
    // テストスイート内で共有するフィールドをここに追加できます
    resource Resource
}

// SetupTest は各テストケースの前に実行されるセットアップ関数です
func (fs *FooSuite) SetupTest() {
    var err error
    fs.resource, err = setupResource()
    if err != nil {
        fs.Fail("Failed to set up: %v", err)
    }
}

// TestFoo は Foo のテストケースです
func (fs *FooSuite) TestFoo() {
    // fs.resource を使ってテストを実行します
    // ...
}

// TestBar は Bar のテストケースです
func (fs *FooSuite) TestBar() {
    // fs.resource を使ってテストを実行します
    // ...
}

// TearDownTest は各テストケースの後に実行されるクリーンアップ関数です
func (fs *FooSuite) TearDownTest() {
    cleanupResource(fs.resource)
}

// TestFooSuite は FooSuite 内の全てのテストメソッドを実行します
func TestFooSuite(t *testing.T) {
    suite.Run(t, new(FooSuite))
}
```

# モックとスタブ
## スタブ
テスト対象のコードが依存している部分をシミュレートするために使用される。（データベースへのアクセスや外部のAPIの呼び出しなど）例えば、外部APIを使用して天気予報を取得する関数をテストする場合、そのAPIへの実際のリクエストを行わずに、スタブを使用して特定の天気データを返すように設定することができます。

## モック
モックは、スタブと同様にテスト対象の依存部分をシミュレートしますが、モックはスタブよりも更に高度な機能を持っています。モックは、メソッドがどのように呼び出されるか（どのパラメータで何回呼び出されるかなど）まで検証するために使用されます。モックを使用すると、テスト対象のコードがその依存部分と正しい方法でやり取りをしていることを確認することができます。

- go-mock
- testify/mock

ここでは gomock で書いてみます。まず interface を定義しましょう。

```go
// repository.go
type DbRepoIf interface {
    CreateArticle(content string) *Ariticle
}
```
モックを作成したインターフェースがあるファイルを`-source`に指定し、mockファイルの名前を`-destination`に指定しましょう。
```bash
mockgen -source=repository.go -destination=db_mock.go -package=main
```
```go
func TestGetData(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockRepo := mock.NewMockDbRepoIf(ctrl)
	mockRepo.EXPECT().CreateArticle("test").Return(&Article{ID: 1, Content: "test"})

	// テストケースを定義
}

```

# 並列テスト
- Go言語のテストフレームワークは並行テストをサポートしています。
- 複数のテストケースを同時に並行して実行することが可能です
- これは、テスト実行時間の短縮や、並行処理に関連する問題を検出するのに有用

テスト関数内でtesting.TのParallelメソッドを呼び出す
```go
func TestAdd(t *testing.T) {
	t.Parallel() // テストを並行実行可能にマーク

	result := Add(1, 2)
	expected := 3
	if result != expected {
		t.Errorf("expected %v, got %v", expected, result)
	}
}

func TestSubtract(t *testing.T) {
	t.Parallel() // テストを並行実行可能にマーク

	result := Subtract(4, 2)
	expected := 2
	if result != expected {
		t.Errorf("expected %v, got %v", expected, result)
	}
}
```