Go言語のTable Driven Testでt.parallel()によるテスト並行実行時、一部のテストケースしか評価されない問題について

## 概要
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261575/bba69736-0f58-8784-3eff-1af1a12c4e4e.png" width=75%>
[Gopher by Renée French, and tenntenn](https://github.com/tenntenn/gopher-stickers/)

Go言語では、テストケースの入力値と期待値を分かりやすくする方法として、**Table Driven Test**(テーブル駆動テスト[^1])が推奨されています。

また、`testing`パッケージに用意されている**t.parallel()メソッド**を使用して、各テストを並行実行[^2]させるといった手法も、テスト時間を短縮するためによく用いられています[^3]。

Table Driven Testと`*testing.T`の`t.parallel()`を組み合わせる方法は、結構あるあるかと思うのですが、テストを書く際に気を付けていないと、**一部のテストケースしか評価されない**という問題が発生してしまいます。

本記事は、上述した問題について

- **どういった場合に発生するのか**

- **どうすれば防げるか**

- **なぜ発生するのか**

を調べ、まとめた内容になります。

## 環境
<img src="https://img.shields.io/badge/Go-1.16.3-00ADD8.svg?logo=go&style=flat">

## どういった場合に発生するのか
簡単な関数のテストを考えます。言語の名前が入ることを期待する`string`型を引数にとり、Gopher(Go開発者)かどうかを判定する関数です。

```go:programmer.go
func IsGopher(language string) bool {
	return language == "GoLang"
}
```
Table Driven Testにのっとって、この関数のテストを書きます。

ここで、今回、テストケースの中には一つだけ**失敗するパターン**を仕込んであります。そのため、このテストは失敗します。

```go
{
  // 失敗することを期待
  name: "Rustaceanの場合、trueを期待する",
  args: args{
    language: "Rust",
  },
  want: true,
},
```

テストコード全体

```go:programmer_test.go
func TestIsGopher(t *testing.T) {
    t.Parallel()
	type args struct {
		language string
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{
			name: "Gopherの場合、trueを期待する",
			args: args{
				language: "GoLang",
			},
			want: true,
		},
		{
            // 失敗する
			name: "Rustaceanの場合、trueを期待する",
			args: args{
				language: "Rust",
			},
			want: true,
		},
		{
			name: "PHPerの場合、falseを期待する",
			args: args{
				language: "PHP",
			},
			want: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
            // サブテスト関数を並行実行させる
			t.Parallel()
			if got := IsGopher(tt.args.language); got != tt.want {
				t.Errorf("IsGopher() = %v, want %v", got, tt.want)
			}
		})
	}
}
```

このテストコードでは、以下のように、`t.Run()`によるサブテスト関数で`t.Parallel()`メソッドを呼びだし、サブテスト関数を並行に実行させるようにしています。

```go
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
                       // サブテスト関数を並行実行させる
                       t.Parallel()
```
さて、テストを走らせます。一つだけ、`Rust`という文字列を渡して`IsGopher`関数が`true`を返すことを期待するテストケースが存在するため、テストは落ちるはずです。

しかし、実行すると、、

```bash
$ go test -v
--- PASS: TestIsGopher (0.00s)
    --- PASS: TestIsGopher/Gopherの場合、trueを期待する (0.00s)
    --- PASS: TestIsGopher/PHPerの場合、falseを期待する (0.00s)
    --- PASS: TestIsGopher/Rustaceanの場合、trueを期待する (0.00s)
PASS
ok
```
テストが通ってしまいました。

## なぜ発生するのか
Table Driven Testでは、テストケースの構造体のスライスに対して`for`ループを回し、各サブテスト関数を順次実行していきます。**しかし、今回の例では、`t.Parallel()`を使用して、テストを並行に実行させるようにしています。**

そのような場合、以下の挙動になります。

- Go言語では、`for`ステートメントはレキシカルブロックを作る。**ループ内の関数値は同じ変数(ループ変数)を「補足」しており、特定の瞬間の値ではなくアドレスを共有している**。

- `t.Parallel()`呼び出しによって、並行実行されるまでサブテスト関数の実行は遅延される。

- 遅延したサブテスト関数が全て実行されるとき、**テストケースの構造体変数`tt`は、`for`ループの最後における値を保持している**。

- そのため、並行実行される全てのサブテスト関数値は`"PHPerの場合、falseを期待する"`になり、失敗するテストケースがあるのにもかかわらず、テストをパスしてしまう。


> 上記について、書籍「Go言語による並行処理」[^4]では、クロージャとゴルーチンとの観点からより詳細に説明されています。適宜ご参照ください。


## どうすれば防げるか
**`for`ループの内側で、`tt`をコピーした変数を宣言する**ことで防ぐことができます。
ループの内側で`tt`を宣言し、外側の`tt`で初期化することで、ループごとの値を保持させるようにします。
> [補足]外側の`tt`はループ変数、forブロック内で宣言した`tt`はローカル変数です。スコープが異なるため、全く別物になります。サブテスト関数の引数には、ループ変数の値をコピーしたローカル変数を渡すようにします。

このとき、コピー先の変数名はコピー元と全く同じ名前を命名することが慣習となっています[^5]。

```diff
	for _, tt := range tests {
+       tt := tt
		t.Run(tt.name, func(t *testing.T) {
                       // サブテスト関数を並行実行させる
                       t.Parallel()
```

テスト実行

```bash
$ go test -v
--- FAIL: TestIsGopher (0.00s)
    --- PASS: TestIsGopher/Gopherの場合、trueを期待する (0.00s)
    --- FAIL: TestIsGopher/Rustaceanの場合、trueを期待する (0.00s)
    --- PASS: TestIsGopher/PHPerの場合、falseを期待する (0.00s)
FAIL
exit status 1
```
正しく動作させることができました。


## まとめ
Table Driven Testで正しくサブテストを並行実行させるためには、"**遅延実行によって、forループ内のクロージャは同じ変数を評価する**"というGo言語の特性を理解しておく必要があります。

まとめると以下の通りです。

- Table Driven Testにおけるサブテスト実行時に**`t.Parallel()`を呼び出すだけだと、ループ最後のテストケース構造体しかテストできない**。

- **ループ内でループ変数を再宣言する**ことにより、遅延したサブテスト関数の関数値が正しくテストケースを実行できるようになる。

### 追記
本記事を執筆中、他の方が[同じ問題についての解決記事](https://zenn.dev/ucwork/articles/cd26d933978080)をご執筆されていることを知りました。直接的な問題の解決法についてはこちらの記事にお譲りし、本記事ではGoのループ変数に対するスコープ規則に絡めた、別アプローチでのアウトプット内容にしました。

間違い等ありましたら、コメント・編集リクエストでご指摘いただければ幸いです！

## 参考情報
https://github.com/golang/go/wiki/TableDrivenTests#parallel-testing

https://engineering.mercari.com/blog/entry/how_to_use_t_parallel/

https://www.maruzen-publishing.co.jp/item/?book_no=295039

https://www.oreilly.com/library/view/go/9784873118468/

---
[^1]: https://github.com/golang/go/wiki/TableDrivenTests
[^2]: この記事における「並行」: Concurrent（並行）複数の動作が、論理的に、順不同もしくは同時に起こりうること(参考：https://qiita.com/yoshinori_hisakawa/items/486752636cf66225483a#%E4%B8%A6%E5%88%97%E5%87%A6%E7%90%86%E3%81%A8%E4%B8%A6%E8%A1%8C%E5%87%A6%E7%90%86%E3%81%AE%E9%81%95%E3%81%84%E3%81%A8%E3%81%AF)
[^3]: `*testing.T`の`t.parallel`メソッドの詳しい内容については、他の方が簡潔に、かつ非常に分かりやすくまとめてくださっています。[Go言語でのテストの並列化 〜t.Parallel()メソッドを理解する〜](https://engineering.mercari.com/blog/entry/how_to_use_t_parallel/)
[^4]: Katherine Cox-Buday　著(山口能迪訳)(2018)『Go言語による並行処理』オライリージャパン, p42。
[^5]: Alan A. A. Donovan and Brian W. Kernighan(柴田芳樹訳)(2016)『プログラミング言語Go』丸善出版,160-161。