🕌
go-cmp/cmp.Diff()でiancoleman/orderedmapのようなunexportedなフィールドだけの値を比較する
cmp.Diffで比較したい
https://github.com/iancoleman/orderedmap で提供されているようなunexportedなフィールドに値を持っている型をgo-cmpのDiff()で比較したい。
直接使うとエラーになる
素直に使うと以下のようなエラーが出る。
consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported
フルのエラーメッセージは以下のようなもの。
--- FAIL: TestIt (0.00s)
--- FAIL: TestIt/simple (0.00s)
panic: cannot handle unexported field at {*orderedmap.OrderedMap}.keys:
"github.com/iancoleman/orderedmap".OrderedMap
consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported [recovered]
panic: cannot handle unexported field at {*orderedmap.OrderedMap}.keys:
"github.com/iancoleman/orderedmap".OrderedMap
consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported
ここで、orderedmapは以下のような定義だった。
type OrderedMap struct {
keys []string
values map[string]interface{}
escapeHTML bool
}
すべてがunexported field。
以下のようなテストコードだった。
package maplib_test
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/iancoleman/orderedmap"
)
func newMap(vs ...any) *orderedmap.OrderedMap {
m := orderedmap.New()
for i := 0; i < len(vs); i += 2 {
m.Set(vs[i].(string), vs[i+1])
}
return m
}
func TestIt(t *testing.T) {
t.Run("simple", func(t *testing.T) {
want := newMap("name", "foo", "age", 20)
got := newMap("name", "foo", "age", 20)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}
Transformerを利用する
エラーメッセージに依ればComparer, ExporterがあるがTransformerが使うと良いかも知れない。
consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported
以下のようにコードを書くと*orderedmap.OrderedMap
をただのmap[string]any
に変換した後に比較してくれるようになるオプションを作る事ができる。
func TestIt(t *testing.T) {
opt := cmp.Transformer("", func(src *orderedmap.OrderedMap) map[string]any {
keys := src.Keys()
dst := make(map[string]any, len(keys))
for _, k := range keys {
v, _ := src.Get(k)
dst[k] = v
}
return dst
})
t.Run("simple", func(t *testing.T) {
want := newMap("name", "foo", "age", 20)
got := newMap("name", "foo", "age", 20)
if diff := cmp.Diff(want, got, opt); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("nested", func(t *testing.T) {
want := newMap("name", "foo", "age", 20, "father", newMap("name", "bar"))
got := newMap("name", "foo", "age", 20, "father", newMap("name", "bar"))
if diff := cmp.Diff(want, got, opt); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}
このようにして作ったものはネストした構造にも対応している。
$ go test -v
=== RUN TestIt
=== RUN TestIt/simple
=== RUN TestIt/nested
--- PASS: TestIt (0.00s)
--- PASS: TestIt/simple (0.00s)
--- PASS: TestIt/nested (0.00s)
失敗させてみる。
=== RUN TestIt/nested
merge_test.go:44: mismatch (-want +got):
(*orderedmap.OrderedMap)(Inverse(maplib_test.TestIt.func1, map[string]any{
"age": int(20),
"father": (*orderedmap.OrderedMap)(Inverse(maplib_test.TestIt.func1, map[string]any{
+ "nam": string("bar"),
- "name": string("bar"),
})),
"name": string("foo"),
}))
Discussion