Goの構造体を柔軟に比較したい
Goの構造体を比較するテストを書くときにgo-cmpパッケージが便利だった。
フィールドの一部を無視して比較する
エンティティに自動生成のID
やタイムスタンプが含まれている場合、例えばtestifyのassert.Equal
などを使って単純に比較するのが難しい。
そこでcmpopts.IgnoreFields
を使って、それらのフィールドを無視して構造体を比較するテストを書いてみる。
time.Now()
によって作成されるCreatedAt
フィールドを無視する例
package main
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestHoge(t *testing.T) {
type Hoge struct {
Name string
CreatedAt time.Time
}
want := Hoge{"Bob", time.Now()}
got := Hoge{"Tom", time.Now()}
opts := []cmp.Option{
cmpopts.IgnoreFields(Hoge{}, "CreatedAt"),
}
if diff := cmp.Diff(want, got, opts...); diff != "" {
t.Errorf("diff: -want, +got:\n%s", diff)
}
}
ログもこんな感じでとても見やすい
--- FAIL: TestHoge (0.00s)
~/dev/go-cmp/main_test.go:26: diff: -want, +got:
main.Hoge{
- Name: "Bob",
+ Name: "Tom",
... // 1 ignored field
}
FAIL
FAIL go-cmp 0.363s
FAIL
フィールドの一部を修正したうえで比較する
こちらはあまり登場機会が多くないかもしれないが、例えば次のようなときに使える。
エンティティがある条件を満たす時だけ、別のエンティティをID参照する例
package main
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
)
type Foo struct {
ID FooID
}
type FooID uuid.UUID
func NewFoo() *Foo {
uuid, _ := uuid.NewUUID()
return &Foo{
ID: FooID(uuid),
}
}
type Hoge struct {
Name string
FooID *FooID
}
func NewHoge(name string) *Hoge {
return &Hoge{
Name: name,
}
}
func CreateHoge(name string) *Hoge {
hoge := NewHoge(name)
// ある条件の時はFooが作られて、HogeはFooをID参照する想定
foo := NewFoo()
hoge.FooID = &foo.ID
return hoge
}
func TestCreateHoge(t *testing.T) {
uuid1, _ := uuid.NewUUID()
id := FooID(uuid1)
want := &Hoge{Name: "Bob", FooID: &id}
got := CreateHoge("Bob")
opts := []cmp.Option{
cmpopts.IgnoreFields(Hoge{}, "FooID"),
}
if diff := cmp.Diff(want, got, opts...); diff != "" {
t.Errorf("diffs: (-want, +got):%s", diff)
}
}
先ほどと同じようにcmpopts.IgnoreFields
を使ってHoge.FooID
を無視することもできるが、テストの信頼性を高めるためにHoge.FooID
が存在することを確かめるテストを書きたい。 (またはその逆)
func TestCreateHoge(t *testing.T) {
uuid1, _ := uuid.NewUUID()
dummyID := FooID(uuid1)
opts := []cmp.Option{
cmpopts.AcyclicTransformer("FooID", func(id *FooID) *FooID {
if id == nil {
return nil
} else {
return &dummyID
}
}),
}
tests := map[string]struct {
want *Hoge
}{
"FooIDが存在してほしいケース": {
want: &Hoge{Name: "Bob", FooID: &dummyID},
},
"FooIDが存在してほしくないケース": {
want: &Hoge{Name: "Bob"},
},
}
for name, tt := range tests {
tt := tt
t.Run(name, func(t *testing.T) {
t.Parallel()
got := CreateHoge("Bob")
if diff := cmp.Diff(tt.want, got, opts...); diff != "" {
t.Errorf("diffs: (-want, +got):\n%s", diff)
}
})
}
}
--- FAIL: TestCreateHoge (0.00s)
--- FAIL: TestCreateHoge/FooIDが存在してほしくないケース (0.00s)
~/dev/go-cmp/main_test.go:79: diffs: (-want, +got):
&main.Hoge{
Name: "Bob",
- FooID: Inverse(FooID, (*main.FooID)(nil)),
+ FooID: Inverse(FooID, &main.FooID{
+ 0xd5, 0xa3, 0x8e, 0x3a, 0x62, 0x30, 0x11, 0xed, 0xa2, 0xb8, 0x6e, 0xde, 0x53,
+ 0x05, 0x13, 0xe7,
+ }),
}
FAIL
FAIL go-cmp 0.335s
FAIL
色々と簡略化したがcmpopts.AcyclicTransformer
を使ってこのように書くことができる
FooID
をダミーに入れ替えることで構造体自体の比較はパスする、そのうえでFooID
が存在してほしいのにnil
のパターン、または逆のパターンの時はテストが落ちるようになる。
あとがき
他のオプションも試してみたい。