goのテスト関連で最近まで知らなかったことをまとめてみた

## はじめに
この記事は [スタンバイ Advent Calendar 2024](https://qiita.com/advent-calendar/2024/stanby) の20日目の記事です。
昨日の記事は [@taca10](https://qiita.com/taca10) さんの「[toolchainの挙動について学んだ](https://taca10.hatenablog.com/entry/2024/12/19/000000?_gl=1*998whq*_gcl_au*MTI4NjIwMjA5OS4xNzMzMzcyNzgy)」でした。

# ごあいさつ
[株式会社スタンバイ](https://stanby.co.jp/)で求人取り込み部分のエンジニアをやっております、[本田](https://qiita.com/arata-honda)と申します!
スタンバイでは、ScalaからGoへのリプレイスやFlinkを用いて大量の求人のストリーム処理などを行っています！
この一年リプレイスや新規開発でGoと触れ合う機会が増え、中でもテスト関連について最近まで知らなかったことがあったので[アドベントカレンダー](https://qiita.com/advent-calendar/2024/stanby)に参加させていただきました！

# goのテスト関連で最近まで知らなかったこと

### TestMainで処理制御できる
結合テストなどのユースケースで、実行前にテーブルレコード追加などをおこなってからテストして、その後削除したいたいなど状態が変わるものについてテストする時にTestMainがよく使われます。

https://pkg.go.dev/testing#hdr-Main

```golang
func TestMain(m *testing.M) {
	setup() // DBのレコード追加などの事前処理
	m.Run() // ここでテストケースが実行される
    teardown() // テーブルの削除などの事後処理
}

func TestAdd(t *testing.T) {
  ....
}
...
```
全体のテストが実行される前後でこのように処理を入れることができます。
Google検索結果で様々なサンプルがありますが、Go 1.15からは[os.Exitが必要なくなっています](https://github.com/golang/go/issues/34129)。

### テストの並列実行

`t.Parallel()`をテストコード内に記述すると、テストケースの並列実行を行ってくれます。

```golang
func TestXXX(t *testing.T) {
	t.Parallel()
  
	t.Run("case1", func(t *testing.T) {
	  t.Parallel()
	  ...
	})
	t.Run("case2", func(t *testing.T) {
	  t.Parallel()
	  ...
	})
  }
```
この場合はサブテスト(t.Runで記述している)とテスト関数が並列実行されます。

また、適切な場所にt.Parallel()がいるかどうかは
https://golangci-lint.run/usage/linters/#paralleltest
golangci-lintなどで静的解析をしてくれるので、テストの実行速度に問題がある場合(例えばテストコードにスリープを入れなければならなく待たないといけない場合に他のテストケースは実行しておいて欲しいなど）に試してみてもいいかもしれないです。

並列化していると下記のように並列実行したケースの終了を待たずにdefer処理が動き出す可能性があります。
そのときはt.Cleanupをつかって事後処理を宣言した方が良いです。

```golang
func TestXXX(t *testing.T) {
	t.Parallel()
    defer teardown() <- ここの処理がt.Parallel()の結果を待たずして実行される
  
	t.Run("case1", func(t *testing.T) {
	  t.Parallel()
	  ...
	})
	t.Run("case2", func(t *testing.T) {
	  t.Parallel()
	  ...
	})
  }
```

```golang
func TestXXX(t *testing.T) {
	t.Parallel()
    t.Cleanup(func() {
        teardown()
    }
  
	t.Run("case1", func(t *testing.T) {
	  t.Parallel()
	  ...
	})
	t.Run("case2", func(t *testing.T) {
	  t.Parallel()
	  ...
	})
  }
```

どの場合にdeferを使っていいのかはメルカリさんの[テックブログ記事](https://engineering.mercari.com/blog/entry/how_to_use_t_parallel/)があったので引用すると以下のようになります。

> ・トップレベルのテスト関数が、t.Run()メソッドによるサブテスト関数を含んでいなければ、defer文あるいはt.Cleanup()メソッドのどちらで後処理を記述してもよい。
> ・トップレベルのテスト関数が、t.Run()メソッドによるサブテスト関数は含んでいて、そのすべてのサブテスト関数がt.Parallel()メソッドを呼び出していない場合、defer文あるいはt.Cleanup()メソッドのどちらで後処理を記述してもよい。
> ・トップレベルのテスト関数が、t.Run()メソッドによるサブテスト関数を含んでいて、少なくともその一つのサブテスト関数がt.Parallel()メソッドを呼び出している場合、t.Cleanup()メソッドで後処理を記述する。


### 構造体の比較

構造体の比較は下記のようにreflection.DeepEqualを使えば良いのはわかっていましたが

```golang
package main

import (
	"reflect"
	"testing"
)

func TestUser(t *testing.T) {
	expect := User{"test", 31, "male"}
	actual := User{"test", 30, "male"}

	if !reflect.DeepEqual(expect, actual) {
		t.Errorf("want %v got %v", expect, actual)
	}
}
```

```shell
$ go test
--- FAIL: TestUser (0.00s)
    main_test.go:13: want {test 31 male} got {test 30 male}
FAIL
exit status 1
FAIL	ftpsample	0.227s
```
まず、どこの構造体の要素が異なっているか、このケースだと値しかなくてパッと見どこのフィールドが違っているか特定するのに時間がかかります。
まず、細かいですが、`%v` -> `%#v`にすることで、どのフィールドの値なのかがわかるようになって可読性が高いです。

```shell
$ go test
--- FAIL: TestUser (0.00s)
    main_test.go:13: want main.User{Name:"test", Age:31, Gender:"male"} got main.User{Name:"test", Age:30, Gender:"male"}
FAIL
exit status 1
FAIL	ftpsample	0.171s
```

今のままだと目視でどこが間違っているか確認する必要があり、差分で出したくなってきます。
構造体の差分（どこが間違っているか）を出したい場合は[go-cmp](https://github.com/google/go-cmp)を使うと差分をきれいに出してくれるようになります。

```golang
package main

import (
	"testing"

	"github.com/google/go-cmp/cmp"
)

func TestUser(t *testing.T) {
	expect := User{"test", 31, "male"}
	actual := User{"test", 30, "male"}
	diff := cmp.Diff(expect, actual)
	if diff != "" {
		t.Errorf("(-expected, +result)\n%s", diff)
	}
}
```

```shell
$ go test
--- FAIL: TestUser (0.00s)
    main_test.go:16: (-expected, +result)
          main.User{
          	Name:   "test",
        - 	Age:    31,
        + 	Age:    30,
          	Gender: "male",
          }
FAIL
exit status 1
FAIL	ftpsample	0.305s
```

ただ、上記のようにエラー文言をカスタマイズするのも面倒というのであれば[testify](https://github.com/stretchr/testify)でも同じようなことができる且つ自前でPrintfを用意しなくてもきれいに差分を出してくれます。

```golang
package main

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestUser(t *testing.T) {
	expect := User{"test", 31, "male"}
	actual := User{"test", 31, "female"}

	assert.Equal(t, expect, actual)
}
```

```shell
$ go test
--- FAIL: TestUser (0.00s)
    main_test.go:13:
        	Error Trace:	/Users/arata.honda/stanby/gosample/main_test.go:13
        	Error:      	Not equal:
        	            	expected: main.User{Name:"test", Age:31, Gender:"male"}
        	            	actual  : main.User{Name:"test", Age:31, Gender:"female"}

        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -3,3 +3,3 @@
        	            	  Age: (int32) 31,
        	            	- Gender: (string) (len=4) "male"
        	            	+ Gender: (string) (len=6) "female"
        	            	 }
        	Test:       	TestUser
FAIL
exit status 1
FAIL	ftpsample	0.443s
```

みやすいと言う観点だと、testifyかなと少し思いますが、「CreatedAtみたいに動的に変わるフィールドをもつ構造体を比較する時に部分一致したい...!!」などの柔軟な比較のオプションなどを行う時はgo-cmpの方が良いみたいで使い分けても良いと思いました。（参考記事: https://sy-tencho.com/posts/go-compare-structs/)

また、go-cmpを使うと[protocol buffer](https://protobuf.dev/)で生成した構造体に関しても比較ができるようになっています。

```golang
package main

import (
	"ftpsample/internal"
	"testing"

	"github.com/google/go-cmp/cmp"
	"google.golang.org/protobuf/testing/protocmp"
)

func TestProtoJob(t *testing.T) {
	expect := internal.Job{JobId: "test", Description: "desc"}
	actual := internal.Job{JobId: "test", Description: "desc12"}

	diff := cmp.Diff(&expect, &actual, protocmp.Transform())
	if diff != "" {
		t.Errorf("(-expected, +result)\n%s", diff)
	}
}
```
protoの生成された構造体の中にフィールドでMessageがあり、refrection.DeepEqualやcmp.Diffを単純に行うとパニックになるケースがあります。([protoの公式ドキュメント](https://protobuf.dev/reference/go/faq/#deepequal))
Diffのオプションにproto.Transform()を渡すことでMessageを`map[string]any`に変換してくれるcmpのOptionを提供してくれます。


また、Diffに各構造体の参照を渡してやる(`&expect`)と、Diffの中でprotoのShallow copyにならず`go vet`で引っ掛からなくなります。[issue](https://github.com/google/go-cmp/issues/280)

```shell
$ go test
--- FAIL: TestProtoJob (0.00s)
    main_test.go:17: (-expected, +result)
          (*internal.Job)(Inverse(protocmp.Transform, protocmp.Message{
          	"@type":       s"Job",
        - 	"description": string("desc"),
        + 	"description": string("desc12"),
          	"jobId":       string("test"),
          }))
FAIL
exit status 1
FAIL	ftpsample	0.298s
```

## 感想
現状の開発でprotocol bufferを使った構造体を比較するときにどうすればもっとわかりやすい差分比較ができるだろうと思ってこの度記事を書かせてもらいましたが意外とテスト関連で知らないことは多く、これからもどんどん知識を深めていければと思いました！

明日は[@mick-az](https://qiita.com/mick-az)の記事がでるそうです！
楽しみですね！