🕌

go-cmp/cmp.Diff()でiancoleman/orderedmapのようなunexportedなフィールドだけの値を比較する

2023/05/03に公開

cmp.Diffで比較したい

https://github.com/iancoleman/orderedmap で提供されているようなunexportedなフィールドに値を持っている型をgo-cmpDiff()で比較したい。

直接使うとエラーになる

素直に使うと以下のようなエラーが出る。

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