はじめに
この記事は スタンバイ Advent Calendar 2024 の20日目の記事です。
昨日の記事は @taca10 さんの「toolchainの挙動について学んだ」でした。
ごあいさつ
株式会社スタンバイで求人取り込み部分のエンジニアをやっております、本田と申します!
スタンバイでは、ScalaからGoへのリプレイスやFlinkを用いて大量の求人のストリーム処理などを行っています!
この一年リプレイスや新規開発でGoと触れ合う機会が増え、中でもテスト関連について最近まで知らなかったことがあったのでアドベントカレンダーに参加させていただきました!
goのテスト関連で最近まで知らなかったこと
TestMainで処理制御できる
結合テストなどのユースケースで、実行前にテーブルレコード追加などをおこなってからテストして、その後削除したいたいなど状態が変わるものについてテストする時にTestMainがよく使われます。
func TestMain(m *testing.M) {
setup() // DBのレコード追加などの事前処理
m.Run() // ここでテストケースが実行される
teardown() // テーブルの削除などの事後処理
}
func TestAdd(t *testing.T) {
....
}
...
全体のテストが実行される前後でこのように処理を入れることができます。
Google検索結果で様々なサンプルがありますが、Go 1.15からはos.Exitが必要なくなっています。
テストの並列実行
t.Parallel()
をテストコード内に記述すると、テストケースの並列実行を行ってくれます。
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をつかって事後処理を宣言した方が良いです。
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()
...
})
}
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を使っていいのかはメルカリさんのテックブログ記事があったので引用すると以下のようになります。
・トップレベルのテスト関数が、t.Run()メソッドによるサブテスト関数を含んでいなければ、defer文あるいはt.Cleanup()メソッドのどちらで後処理を記述してもよい。
・トップレベルのテスト関数が、t.Run()メソッドによるサブテスト関数は含んでいて、そのすべてのサブテスト関数がt.Parallel()メソッドを呼び出していない場合、defer文あるいはt.Cleanup()メソッドのどちらで後処理を記述してもよい。
・トップレベルのテスト関数が、t.Run()メソッドによるサブテスト関数を含んでいて、少なくともその一つのサブテスト関数がt.Parallel()メソッドを呼び出している場合、t.Cleanup()メソッドで後処理を記述する。
構造体の比較
構造体の比較は下記のようにreflection.DeepEqualを使えば良いのはわかっていましたが
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)
}
}
$ 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
にすることで、どのフィールドの値なのかがわかるようになって可読性が高いです。
$ 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を使うと差分をきれいに出してくれるようになります。
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)
}
}
$ 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でも同じようなことができる且つ自前でPrintfを用意しなくてもきれいに差分を出してくれます。
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)
}
$ 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で生成した構造体に関しても比較ができるようになっています。
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の公式ドキュメント)
Diffのオプションにproto.Transform()を渡すことでMessageをmap[string]any
に変換してくれるcmpのOptionを提供してくれます。
また、Diffに各構造体の参照を渡してやる(&expect
)と、Diffの中でprotoのShallow copyにならずgo vet
で引っ掛からなくなります。issue
$ 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の記事がでるそうです!
楽しみですね!