🙄

go-cmpの各Option紹介

2021/09/13に公開

go-cmp

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

単体テストでオブジェクトを比較する場合reflect.DeepEqualを使用するのが一般的です。
reflect.DeepEqualはオブジェクト完全に一致していないとfalseになってしまいますが、
実際にオブジェクトを比較するときは、以下のように柔軟に比較したいことがよくあります。

  • privateフィールドは比較したくない or 比較したい
  • 時間のフィールドは比較したくない
  • 順番が一致していなくてもSortして一致していれば問題ない

また大きなオブジェクトをreflect.DeepEqualで比較すると、差分を調べるのがとても大変です。
そういった細かい条件を設定してオブジェクトを比較してくれるのがgo-cmpです。

複雑なオブジェクトを比較してみる

reflect.DeepEqualと異なり、差分がとても見やすいのところがgo-cmpの魅力の一つです。

func TestCmp(t *testing.T) {
	type ComplexObject struct {
		ID          int64
		Name        string
		Score       float64
		Public      bool
		RelationIds []int64
	}
	type ComplexObjects []ComplexObject

	type Compare struct {
		Name    string
		Value   int
		Objects ComplexObjects
	}

	v1 := &Compare{
		Name:  "Tom",
		Value: 100,
		Objects: ComplexObjects{
			{
				ID:          1,
				Name:        "xyz",
				Score:       1.0,
				Public:      true,
				RelationIds: []int64{1, 2, 3, 4, 5},
			},
			{
				ID:          2,
				Name:        "xxxxx",
				Score:       99.9,
				Public:      true,
				RelationIds: []int64{10, 20, 30, 40, 50},
			},
		},
	}
	v2 := &Compare{
		Name:  "Andrew",
		Value: 50,
		Objects: ComplexObjects{
			{
				ID:          1,
				Name:        "xyz",
				Score:       1.0,
				Public:      true,
				RelationIds: []int64{1, 2, 3, 4, 5},
			},
			{
				ID:          3,
				Name:        "abc",
				Score:       11.1,
				Public:      false,
				RelationIds: []int64{6, 7, 8, 9, 10},
			},
		},
	}
	if diff := cmp.Diff(v1, v2); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- FAIL: TestCmp (0.00s)
example_test.go:68: Compare value is mismatch (-v1 +v2):  &gocmp.Compare{
        - 	Name:  "Tom",
        + 	Name:  "Andrew",
        - 	Value: 100,
        + 	Value: 50,
          	Objects: gocmp.ComplexObjects{
          		{ID: 1, Name: "xyz", Score: 1, Public: true, ...},
        - 		{
        - 			ID:          2,
        - 			Name:        "xxxxx",
        - 			Score:       99.9,
        - 			Public:      true,
        - 			RelationIds: []int64{10, 20, 30, 40, 50},
        - 		},
        + 		{ID: 3, Name: "abc", Score: 11.1, RelationIds: []int64{6, 7, 8, 9, 10}},
          	},
          }

--- FAIL: TestCmp (0.00s)
FAIL
FAIL	github.com/tMinamiii/sandbox-go/gocmp	0.002s

各種オプション紹介

go-cmpには様々な機能があります。今回はgo-cmpが比較の際に使用する各Optionの使い方をサンプルコードを添えて紹介しようとおもいます。

cmp.AllowUnexported()

構造体がprivateフィールドを持つ場合は、以下のオプションのどちらかを指定する必要があります。

  • cmp.AllowUnexported
  • cmpopts.IgnoreUnexported

cmp.AllowUnexportedはprivateフィールを比較対象にする場合に使用するオプションです。下記のテストではprivateフィールドが異なるためテストが失敗します。

func TestAllowUnexported(t *testing.T) {
	type Compare struct {
		Exported   int
		unexported int
	}

	v1 := &Compare{
		Exported:   100,
		unexported: 1,
	}
	v2 := &Compare{
		Exported:   100,
		unexported: 2,
	}
	opt := cmp.AllowUnexported(Compare{})
	if diff := cmp.Diff(v1, v2, opt); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- FAIL: TestAllowUnexported (0.00s)
    example_test.go:25: Compare value is mismatch (-v1 +v2):  &gocmp.Compare{
                Exported:   100,
        -       unexported: 1,
        +       unexported: 2,
          }

FAIL
FAIL    github.com/tMinamiii/sandbox-go/gocmp   0.003s

cmpopts.AcyclicTransformer

cmpopts.AcyclicTransformerはオブジェクトの指定したフィールドに変換処理を施した後に比較することができます。 下記のサンプルコードでは、オブジェクトのSplitLinesフィールを,で分割したのちに比較しています。

下記のサンプルではあえてエラーになる値で実験します。 分割なしと分割ありでエラー差分のメッセージがことなります。 分割なしでは「Sliceが異なる」というメッセージですが、分割ありでは「Slice内のある文字(要素)が異なる」に変わります。 長いデータを比較する際は、cmpopts.AcyclicTransformerを使用して分割すると、差分を見つけやすくなると思われます。

分割なし

func TestAcyclicTransformer(t *testing.T) {
	type Compare struct {
		SplitLines interface{}
	}
	v1 := &Compare{
		SplitLines: "1,2,3",
	}
	v2 := &Compare{
		SplitLines: "1,10,3",
	}

	// func AcyclicTransformer(name string, xformFunc interface{}) cmp.Option
	opts := []cmp.Option{
		// cmpopts.AcyclicTransformer("SplitLines", func(s string) []string {
		// 	return strings.Split(s, ",")
		// }),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
example_test.go:445: Compare value is mismatch (-v1 +v2):  &gocmp.Compare{
        - 	SplitLines: string("1,2,3"),
        + 	SplitLines: string("1,10,3"),
          }

--- FAIL: TestAcyclicTransformer (0.00s)
FAIL
FAIL	github.com/tMinamiii/sandbox-go/gocmp	0.002s

分割あり

func TestAcyclicTransformer(t *testing.T) {
	type Compare struct {
		SplitLines interface{}
	}
	v1 := &Compare{
		SplitLines: "1,2,3",
	}
	v2 := &Compare{
		SplitLines: "1,10,3",
	}

	// func AcyclicTransformer(name string, xformFunc interface{}) cmp.Option
	opts := []cmp.Option{
		cmpopts.AcyclicTransformer("SplitLines", func(s string) []string {
			return strings.Split(s, ",")
		}),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
example_test.go:446: Compare value is mismatch (-v1 +v2):  &gocmp.Compare{
          	SplitLines: string(Inverse(SplitLines, []string{
          		"1",
        - 		"2",
        + 		"10",
          		"3",
          	})),
          }

--- FAIL: TestAcyclicTransformer (0.00s)
FAIL
FAIL	github.com/tMinamiii/sandbox-go/gocmp	0.002s

IgnoreXXXX

cmpopts.IgnoreUnexported

cmp.AllowUnexportedではprivateフィールドも比較したためテストが失敗しました。 今度はcmpops.IgnoreUnexportedを使用してみます。 privateフィールドの比較がなくなったのでテストが通ります。

func TestIgnoreUnexported(t *testing.T) {
	type Compare struct {
		Exported   int
		unexported int
	}

	v1 := &Compare{
		Exported:   100,
		unexported: 1,
	}
	v2 := &Compare{
		Exported:   100,
		unexported: 2,
	}
	// func IgnoreUnexported(typs ...interface{}) cmp.Option
	opt := cmpopts.IgnoreUnexported(Compare{})
	if diff := cmp.Diff(v1, v2, opt); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- PASS: TestIgnoreUnexported (0.00s)
PASS
ok  	github.com/tMinamiii/sandbox-go/gocmp

cmpopts.IgnoreFields

オブジェクトのフィールドに時間が含まれているときは、比較対象から外したいことがあります。 cmpopts.IgnoreFieldsを使用すると特定のフィールドのみ比較対象から外すことができます。

func TestIgnoreFields(t *testing.T) {
	type Compare struct {
		Exported  int
		CreatedAt time.Time
		UpdatedAt time.Time
	}

	v1 := &Compare{
		Exported:  100,
		CreatedAt: time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC),
		UpdatedAt: time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC),
	}
	v2 := &Compare{
		Exported:  100,
		CreatedAt: time.Date(3100, 1, 1, 0, 0, 0, 0, time.UTC),
		UpdatedAt: time.Date(3100, 1, 1, 0, 0, 0, 0, time.UTC),
	}
	// func IgnoreFields(typ interface{}, names ...string) cmp.Option
	opts := []cmp.Option{
		cmpopts.IgnoreFields(Compare{}, "CreatedAt", "UpdatedAt"),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- PASS: TestIgnoreFields (0.00s)
PASS
ok  	github.com/tMinamiii/sandbox-go/gocmp	0.002s

cmpopts.IgnoreTypes

さきほどはフィールド名をしてして、比較対象から外しましたが、型を指定して比較対象から外すこともできます。cmpopts.IgnoreTypesを使用してtime.Timeを外すことで異なる時間をもったオブジェクトを比較してもテストが通ります。

func TestIgnoreTypes(t *testing.T) {
	type Compare struct {
		Exported  int
		CreatedAt time.Time
		UpdatedAt time.Time
	}

	v1 := &Compare{
		Exported:  100,
		CreatedAt: time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC),
		UpdatedAt: time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC),
	}
	v2 := &Compare{
		Exported:  100,
		CreatedAt: time.Date(3100, 1, 1, 0, 0, 0, 0, time.UTC),
		UpdatedAt: time.Date(3100, 1, 1, 0, 0, 0, 0, time.UTC),
	}
	// func IgnoreTypes(typs ...interface{}) cmp.Option
	opts := []cmp.Option{
		cmpopts.IgnoreTypes(time.Time{}),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- PASS: TestIgnoreTypes (0.00s)
PASS
ok  	github.com/tMinamiii/sandbox-go/gocmp	0.003s

cmpopts.IgnoreSliceElements

cmpopts.IgnoreSliceElementsを使用するとsliceの特定の要素を無視して比較することもできます。下記のサンプルはsliceの9を無視して比較したものです。v2のSliceは9を無視すると1,2,3,4,5になり一致します

func TestIgnoreSliceElements(t *testing.T) {
	type Compare struct {
		Exported int
		Slice    []int
	}

	v1 := &Compare{
		Exported: 100,
		Slice:    []int{1, 2, 3, 4, 5},
	}
	v2 := &Compare{
		Exported: 100,
		Slice:    []int{1, 9, 2, 9, 3, 9, 4, 9, 5},
	}
	// func IgnoreSliceElements(discardFunc interface{}) cmp.Option
	opts := []cmp.Option{
		cmpopts.IgnoreSliceElements(func(elem int) bool {
			return elem == 9
		}),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- PASS: TestIgnoreSliceElements (0.00s)
PASS
ok  	github.com/tMinamiii/sandbox-go/gocmp

cmpopts.IgnoreMapEntries

func TestIgnoreMapEntries(t *testing.T) {
	type Compare struct {
		Exported int
		Map      map[string]int
	}

	v1 := &Compare{
		Exported: 100,
		Map: map[string]int{
			"A": 1,
			"B": 1,
		},
	}
	v2 := &Compare{
		Exported: 100,
		Map: map[string]int{
			"B": 1,
			"A": 1,
			"X": 9999,
		},
	}
	// func IgnoreMapEntries(discardFunc interface{}) cmp.Option
	opts := []cmp.Option{
		cmpopts.IgnoreMapEntries(func(key string, val int) bool {
			return key == "X" && val == 9999
		}),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- PASS: TestIgnoreMapEntries (0.00s)
PASS
ok  	github.com/tMinamiii/sandbox-go/gocmp	0.003s

cmpopts.cmpopts.IgnoreInterfaces

cmpopts.cmpopts.IgnoreInterfaces は、比較対象のオブジェクトから特定のInterfaceを比較しないよう除外設定できます。cmpopts.IgnoreInterfaces(struct{ InterfaceA }{}) のように設定するとCompareオブジェクトを比較する際ににInterfaceA型フィールドを無視して比較することができます。

type InterfaceA interface {
	Get() int
}

type InterfaceAImpl struct {
	X int
}

func (i *InterfaceAImpl) Get() int {
	return i.X
}

type InterfaceB interface {
	Set(x int)
}

type InterfaceBImpl struct {
	X int
}

func (i *InterfaceBImpl) Set(x int) {
	i.X = x
}

func TestIgnoreInterfaces(t *testing.T) {

	type Compare struct {
		Exported int
		IA       InterfaceA
		IB       InterfaceB
	}

	v1 := &Compare{
		Exported: 100,
		IA:       &InterfaceAImpl{X: 1},
		IB:       &InterfaceBImpl{X: 1},
	}
	v2 := &Compare{
		Exported: 100,
		IA:       &InterfaceAImpl{X: 100},
		IB:       &InterfaceBImpl{X: 1},
	}
	// func IgnoreInterfaces(ifaces interface{}) cmp.Option
	opts := []cmp.Option{
		cmpopts.IgnoreInterfaces(struct{ InterfaceA }{}),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- PASS: TestIgnoreInterfaces (0.00s)
PASS
ok  	github.com/tMinamiii/sandbox-go/gocmp	0.002s

EquoteXXX

cmpopts.EquateEmpty

cmpopts.EquateEmpty を使用すると空のMapやSliceとnilを同値とみなして比較することができます。

func TestEquoteEmpty(t *testing.T) {

	type Compare struct {
		Exported int
		Slice    []int
		Map      map[string]int
	}

	v1 := &Compare{
		Exported: 100,
		Slice:    []int{},
		Map:      map[string]int{},
	}
	v2 := &Compare{
		Exported: 100,
		Slice:    nil,
		Map:      nil,
	}
	// func EquateEmpty() cmp.Option
	opts := []cmp.Option{
		cmpopts.EquateEmpty(),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- PASS: TestEquoteEmpty (0.00s)
PASS
ok  	github.com/tMinamiii/sandbox-go/gocmp	0.002s

cmpopts.EquateNaNs

cmpopts.EquateNaNsはfloat32/float64のNaNを同値として扱って比較してくれます。
v1もv2もmath.NaN()なのだからオプション無しでも良いのでは?と思うかもしれませんがオプションを設定しないとテストは失敗します。

オプションなし
func TestEquoteNans(t *testing.T) {

	type Compare struct {
		Exported float64
	}

	v1 := &Compare{
		Exported: math.NaN(),
	}
	v2 := &Compare{
		Exported: math.NaN(),
	}
	// func EquateNaNs() cmp.Option
	opts := []cmp.Option{
		// cmpopts.EquateNaNs(),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
=== RUN   TestEquoteNans
example_test.go:317: Compare value is mismatch (-v1 +v2):  &gocmp.Compare{
        - 	Exported: NaN,
        + 	Exported: NaN,
          }

--- FAIL: TestEquoteNans (0.00s)
FAIL
FAIL	github.com/tMinamiii/sandbox-go/gocmp	0.003s
オプションあり
func TestEquoteNans(t *testing.T) {

	type Compare struct {
		Exported float64
	}

	v1 := &Compare{
		Exported: math.NaN(),
	}
	v2 := &Compare{
		Exported: math.NaN(),
	}
	// func EquateNaNs() cmp.Option
	opts := []cmp.Option{
		cmpopts.EquateNaNs(),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- PASS: TestEquoteNans (0.00s)
PASS
ok  	github.com/tMinamiii/sandbox-go/gocmp	(cached)

cmpopts.EquateErrors

go-cmpはデフォルトではerrorの比較ができません。そこでcmpopts.EquateErrorsを使用するとエラーを比較できるようになります。同値と見なされるには、同じエラーオブジェクトである必要があります。(メッセージだけ一致していても比較失敗します)

オプションなし
func TestEquoteErrors(t *testing.T) {

	type Compare struct {
		Error error
	}
	err := errors.New("error occured")
	v1 := &Compare{
		Error: err,
	}
	v2 := &Compare{
		Error: err,
	}
	// func EquateErrors() cmp.Option
	opts := []cmp.Option{
		// cmpopts.EquateErrors(),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- FAIL: TestEquoteErrors (0.00s)
panic: cannot handle unexported field at {*gocmp.Compare}.Error.(*errors.errorString).s:
	"errors".errorString
consider using cmpopts.EquateErrors to compare error values [recovered]
	panic: cannot handle unexported field at {*gocmp.Compare}.Error.(*errors.errorString).s:
	"errors".errorString
consider using cmpopts.EquateErrors to compare error values

オプションあり 別オブジェクト
func TestEquoteErrors(t *testing.T) {
	type Compare struct {
		Error error
	}
	v1 := &Compare{
		Error:errors.New("error occured"),
	}
	v2 := &Compare{
		Error: errors.New("error occured"),
	}
	// func EquateErrors() cmp.Option
	opts := []cmp.Option{
		cmpopts.EquateErrors(),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
example_test.go:339: Compare value is mismatch (-v1 +v2):  &gocmp.Compare{
        - 	Error: &⟪0xc00006e630⟫"errors".errorString{s: "error occured"},
        + 	Error: &⟪0xc00006e650⟫"errors".errorString{s: "error occured"},
          }

--- FAIL: TestEquoteErrors (0.00s)
FAIL
FAIL	github.com/tMinamiii/sandbox-go/gocmp	0.003s
オプションあり
func TestEquoteErrors(t *testing.T) {

	type Compare struct {
		Error error
	}
	err := errors.New("error occured")
	v1 := &Compare{
		Error: err,
	}
	v2 := &Compare{
		Error: err,
	}
	// func EquateErrors() cmp.Option
	opts := []cmp.Option{
		cmpopts.EquateErrors(),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- PASS: TestEquoteErrors (0.00s)
PASS
ok  	github.com/tMinamiii/sandbox-go/gocmp

cmpopts.EquateApprox(fraction, margin float64)

cmpopts.EquateApprox(fraction, margin float64) は少数の差が一定の範囲内ならば同値とみなす設定ができます。引数のfractionとmarginは右式のように使用されます|x-y| ≤ max(fraction*min(|x|, |y|), margin)

  • fraction=0, margin=0.01 にすると、差分が0.01内であれば同値と見なします。
  • fraction=10, margin=0にすると、差分が1/10であれば同値とみなします。
func TestEquoteApprox(t *testing.T) {

	type Compare struct {
		Exported float64
	}
	v1 := &Compare{
		Exported: 0.1,
	}
	v2 := &Compare{
		Exported: 0.01,
	}
	// func EquateApprox(fraction, margin float64) cmp.Option
	opts := []cmp.Option{
		cmpopts.EquateApprox(0, 0.091),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- PASS: TestEquoteApprox_fraction_margin (0.00s)
PASS
ok  	github.com/tMinamiii/sandbox-go/gocmp
func TestEquoteApprox(t *testing.T) {

	type Compare struct {
		Exported float64
	}
	v1 := &Compare{
		Exported: 0.1,
	}
	v2 := &Compare{
		Exported: 0.01,
	}
	// func EquateApprox(fraction, margin float64) cmp.Option
	opts := []cmp.Option{
		cmpopts.EquateApprox(10, 0),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- PASS: TestEquoteApprox_fraction_margin (0.00s)
PASS
ok  	github.com/tMinamiii/sandbox-go/gocmp

cmpopts.EquateApproxTime

cmpopts.EquateApproxTimeは、時間を比較する際にその差が、引数の時間(time.Duration)以内であれば同値と見なすことができます

func TestEquoteApproxTime(t *testing.T) {

	type Compare struct {
		Exported time.Time
	}
	v1 := &Compare{
		Exported: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
	}
	v2 := &Compare{
		Exported: time.Date(2021, 1, 1, 0, 0, 0, 10, time.UTC),
	}
	// func EquateApproxTime(margin time.Duration) cmp.Option
	opts := []cmp.Option{
		cmpopts.EquateApproxTime(time.Second * 10),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- PASS: TestEquoteApproxTime (0.00s)
PASS
ok  	github.com/tMinamiii/sandbox-go/gocmp	0.0

SortXXXX

cmpopts.SortSlices

cmpopts.SortSlicesは引数で渡した関数でSliceをソートしてから比較することができます。引数でわたす無名関数の型と異なるSliceはソートされないため、下記の例に順番が異なるstringのSliceを追加すると同値にならずエラーになります。

func TestSortSlice(t *testing.T) {

	type Compare struct {
		NumSlice []int
	}
	v1 := &Compare{
		NumSlice: []int{1, 2, 3, 4, 5},
	}
	v2 := &Compare{
		NumSlice: []int{5, 4, 3, 2, 1},
	}
	// func SortSlices(lessFunc interface{}) cmp.Option
	opts := []cmp.Option{
		cmpopts.SortSlices(func(i, j int) bool {
			return i < j
		}),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
--- PASS: TestSortSlice (0.00s)
PASS
ok  	github.com/tMinamiii/sandbox-go/gocmp	0.002
func TestSortSlice(t *testing.T) {

	type Compare struct {
		NumSlice    []int
		StringSlice []string
	}
	v1 := &Compare{
		NumSlice:    []int{1, 2, 3, 4, 5},
		StringSlice: []string{"1", "2", "3"},
	}
	v2 := &Compare{
		NumSlice:    []int{5, 4, 3, 2, 1},
		StringSlice: []string{"3", "2", "1"},
	}
	// func SortSlices(lessFunc interface{}) cmp.Option
	opts := []cmp.Option{
		cmpopts.SortSlices(func(i, j int) bool {
			return i < j
		}),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}
example_test.go:403: Compare value is mismatch (-v1 +v2):  &gocmp.Compare{
          	NumSlice: Inverse(cmpopts.SortSlices, []int{1, 2, 3, 4, ...}),
          	StringSlice: []string{
        - 		"1",
        + 		"3",
          		"2",
        - 		"3",
        + 		"1",
          	},
          }

--- FAIL: TestSortSlice (0.00s)
FAIL
FAIL	github.com/tMinamiii/sandbox-go/gocmp	0.003s

cmpopts.SortMaps

cmpopts.SortMapsは引数で渡した関数でMapをソートしてから比較することができます。ソートに用いるのはmapのKey値です。下記のサンプルだとstring型なのでcmpopts.SortMaps(func(i, j string) boolという関数でソートします。ただし、Mapが同じかどうかに要素順番は無関係なので、cmpopts.SortMapsは比較目的ではなく別の用途で使われると思われます。

func TestSortMaps(t *testing.T) {
	v1 := map[string]int{
		"AAA": 111,
		"BBB": 222,
		"CCC": 333,
	}
	v2 := map[string]int{
		"BBB": 222,
		"CCC": 333,
		"AAA": 111,
	}

	// func SortMaps(lessFunc interface{}) cmp.Option
	opts := []cmp.Option{
		cmpopts.SortMaps(func(i, j string) bool {
			return i < j
		}),
	}
	if diff := cmp.Diff(v1, v2, opts...); diff != "" {
		t.Errorf("Compare value is mismatch (-v1 +v2):%s\n", diff)
	}
}

おわりに

検索してもサンプルコードや情報が少ないので全オプションを紹介してみました。今回はオプションのみの紹介ですが、Diff以外の比較機能についても後日紹介しようとおもいます。 go-cmpを使いこなして可読性の高いテストコードを作成しましょう!

Discussion