golangテストで使えるライブラリ覚え書き

この記事は[and factory Advent Calendar 2020](https://qiita.com/advent-calendar/2020/andfactory)の21日目の記事です。
昨日は@tsumuchanさんの「[【Android】独自のConstraintHelperを作成し、MotionSceneをちょっとスッキリさせてみる](https://qiita.com/tsumuchan/items/d723941341c0ea773e8d)」でした。

# はじめに

[Go Conference'20 in Autumn SENDAI](https://sendai.gocon.jp/)で発表された「[DRY & 型安全にテスト用structを初期化しよう](https://www.youtube.com/watch?v=DkWUh4oR0ac)」を見た際、「そういえばテストで使われてそうなライブラリとか何も知らんな」と思ったので、せめて有名どころや発表内で紹介されてたものは知っておきたい…ということで調べて試してみました。
雰囲気で & 愚直にテストを書いている自覚があるので、既存のライブラリ等を知って少しでも効率的にできればなと思った次第です。
同じように雰囲気でテスト書いてる方の助力になれれば幸いです。

# go test
言わずと知れたGoのテスト用標準ライブラリ。
こちらに関しては使い方含めていろいろな記事があるでしょうし、説明は省きます。

テストの雛形を作るのが手作業だとまずダルいのですが、VSCodeであればコマンドパレットから`Generate Unit Tests For ...`を実行してテストコードの雛形を作成することができます。
![スクリーンショット 2020-12-19 22.43.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491167/bfe4ed04-b55f-cd56-a955-46444e742982.png)
関数・ファイル単位の雛形であればコードを開いて右クリックでも雛形作成のメニューが出てきます。

書いてて気づいたんですがカバレッジを取れるコマンド(`Toggle Test Coverage In Current Package`)もあるんですね。
![スクリーンショット 2020-12-19 23.18.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491167/edb96aed-4b1c-24da-4f87-938700e5db69.png)
テストが書けていないところは赤色になるので、どこの分岐のテストが足りてないかを手元で確認するときなどに使えそうです。

# google/go-cmp

https://github.com/google/go-cmp

Google非公式の値比較のライブラリ。
標準の比較機能である`reflect.DeepEqual`を使用すれば構造体の比較等も行えるのですが、unexportedなフィールドやTime型のフィールドが一致せずテストがPASSしないといった問題点があります。go-cmpを使用することでより柔軟な比較を行うことができます。

使用方法としては以下のような感じ。

```golang
package main

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

func Test_getResults(t *testing.T) {
	tests := []struct {
		name string
		want TestStruct
	}{
		{
			name: "test_1",
			want: TestStruct{
				Name:      "Test",
				Numbers:   []int{1, 2, 3, 4, 5},
				CreatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := getResults()
			if diff := cmp.Diff(got, tt.want); diff != "" {
				t.Errorf(diff)
			}
		})
	}
}

```

`cmp.Diff()`に比較したい構造体を渡せば良いだけです。差異がない場合は空文字が返ってきます。

差異がある場合は以下のような結果が返却されます。異なっている部分には`+`、`-`がつくので、`reflect.DeepEqual`の結果よりも直感的に見やすいです。

```
=== RUN   Test_getResults/test_1
    test_test.go:28:   main.TestStruct{
                Name: "Test",
                Numbers: []int{
                        ... // 2 identical elements
                        3,
                        4,
        -               5,
        +               6,
                },
                CreatedAt: s"2020-01-01 00:00:00 +0000 UTC",
          }
--- FAIL: Test_getResults (0.00s)
```

go-cmpではオプションが設定でき、特定のフィールドの比較を無視したり、unexportedなフィールドの比較をON/OFFできたりと、用途に合わせてカスタマイズすることができます。
オプションについては「[go-cmpを使う理由とTipsの紹介](https://tutuz-tech.hatenablog.com/entry/2020/12/07/220236)」で読みやすく、必要なものがまとめられていました。


# go mock

https://github.com/golang/mock

インタフェースのmockを作るためのライブラリ。

インストールは

```
$ go get github.com/golang/mock/gomock
$ go get github.com/golang/mock/mockgen
```
で行い、以下のようにインタフェースに対してコマンドを実行すると、モックを作成してくれます。

```
$ mockgen -source sample.go
```

インストール方法から使い方まで、「[Go Mockでインタフェースのモックを作ってテストする #golang](https://qiita.com/tenntenn/items/24fc34ec0c31f6474e6d)」で分かりやすくまとめられています。


# testfixutures

https://github.com/go-testfixtures/testfixtures

yamlファイルからDBのデータを作成することで、テスト前にDBのクリーンアップ & テストデータのロードを行えるライブラリ。
Ruby on Railsのフィクスチャ(サンプルデータ)がyaml形式のファイルとして保存されていることから発想を得たそうです。yamlファイルにサンプルデータを記述しておくことで、特定のDBに依存しないデータを用意することができます。

使用する場合はまず任意のディレクトリにyamlファイルを用意します。

```yaml:testdata/fixtures/greetings.yml
- id: 1
  msg: Good Morning
  created_at: RAW=NOW()
  updated_at: RAW=NOW()
  deleted_at: NULL
```

１テーブルごとに１ファイル用意する必要があり、ファイル名はテーブルの名前をつける必要があります。
今回はgreetingsというテーブルにデータを入れますので、`greetings.yml`というサンプルデータを`testdata/fixtures`ディレクトリ配下に用意しました。
`NOW()`などの関数やSQL文を使いたい場合は`RAW=`を指定、NULLを入れたい場合は`NULL`と指定すればいい感じに値を突っ込んでくれるようです。

テスト実行時には、以下のようにymlファイルを読み込んでデータをテーブルに突っ込みます。

```go
package main

import (
	_ "github.com/go-sql-driver/mysql"

	"github.com/go-testfixtures/testfixtures/v3"
)

func Test_getGreeting(t *testing.T) {
	type args struct {
		t time.Time
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		// Some tests
	}

	db, err := sql.Open("mysql", "root:@/test")
	if err != nil {
		fmt.Println(err)
	}
	defer db.Close()

	fixtures, err := testfixtures.New(
		testfixtures.Database(db),
		testfixtures.Dialect("mysql"),
		testfixtures.Directory("testdata/fixtures"),
	)
	if err != nil {
		fmt.Println(err)
	}

	for _, tt := range tests {
		if err := fixtures.Load(); err != nil {
			fmt.Println(err)
		}

		t.Run(tt.name, func(t *testing.T) {
			// Run tests
		})
	}
}
```

DBへの接続を開いて、`testfixtures.New()`でDBへのコネクション、DBの種類、サンプルデータが入っているディレクトリを指定します。今回はディレクトリでサンプルデータを読み込んでいますが、特定のファイルのみを読み込むなどの調整もできるようです。
`fixtures.Load()`でテーブルからのデータの削除 & テーブルへのデータの追加を行ってくれるので、特段テストごとにデータを変える必要がなければ各テストの前処理として実行するとよさそうです。

# factory-go

https://github.com/bluele/factory-go

複雑な構造体を簡単に作成するためのライブラリ。
構造体を作るファクトリを定義することで柔軟にデータが作れるので、構造体の構成が変わっちゃったりしたときにもメンテナンスを容易にできます。pythonに[factory_boy](https://github.com/FactoryBoy/factory_boy)というライブラリがあり、そこから着想を得たそう。

```golang
package main

import (
	"github.com/Pallinder/go-randomdata"
	"github.com/bluele/factory-go/factory"
)

type User struct {
	ID       int
	Name     string
	Location string
}

// 'Location: "Tokyo"' is default value.
var UserFactory = factory.NewFactory(
	&User{Location: "Tokyo"},
).SeqInt("ID", func(n int) (interface{}, error) {
	return n, nil
}).Attr("Name", func(args factory.Args) (interface{}, error) {
	user := args.Instance().(*User)
	return fmt.Sprintf("user-%d", user.ID), nil
})

func main() {
	for i := 0; i < 3; i++ {
		user := UserFactory.MustCreate().(*User)		
		fmt.Println("ID:", user.ID, " Name:", user.Name, " Location:", user.Location)
	}
}
```

テストで使用する構造体を定義して、`factory.NewFactory()`でその中身を作るための式を定義する、という感じのようです。
for文の中で`MustCreate()`を使って構造体を生成すれば、自動的に`ID`などがセットされていきます。上記の`Location`のように固定値を入れたりすることもできます。
ちなみにファクトリを使って作る構造体には`ID`が必須なようで、ないとエラーが出ます。基本的にはIDにセットされた値を使って各フィールドの値を作っていくような書き方になりそう。

使ってみた感じちょっと可読性が低い感じがしますね。。
構造体の中に構造体が入っているようなものやフィールド数が多いものを作ろうとしたり、IDに関係ない値をセットしたい(IDが1だったらLocationがTokyo、2だったらChinaとか)となると、結構複雑になってしんどいかも。


# おわりに
業務でテストを書くことがありますが、今でも結構testingパッケージの機能を把握しきれていなかったり、テストケースをひたすら書いていると面倒臭くなったり、途中で放置してどこまで書いたんだか分からなくなったりしてます。。

今回調査したのは有名どころばかりという感じですが、他のライブラリにも目を光らせつつ、適宜取り入れてなんとか楽ができればいいな〜！と思います。

明日は@cpp0302さんです！

## 参考
* [DRY & 型安全にテスト用structを初期化しよう](https://www.youtube.com/watch?v=DkWUh4oR0ac)
* [春の入門祭り 🌸 #01 Goのテストに入門してみよう！](https://future-architect.github.io/articles/20200601/)
* [GoのAPI開発現場におけるユニットテストTips](https://devblog.thebase.in/entry/2018/12/04/110000)
* [go-cmpを使う理由とTipsの紹介](https://tutuz-tech.hatenablog.com/entry/2020/12/07/220236)