45
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

go-cmp を実際に使ってみた

Last updated at Posted at 2018-11-07

go でテストケース書いていて、 reflect.DeepEqual を使っていると time.Time の比較なんかで死んでしまいます。
対策を検索すると、 go-cmp 使えよ! っていう記事は見かけるものの、実際にどういうふうに書けばよいのかいまいちわからないし、Exampleを読んでもうーん?ってなったので、まあこれは一度使ってみましょう。
ということで使ってみた記録です。

とりあえず diff を取ってみる

package main

import (
	"fmt"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
)

type S1 struct {
	F1 int
}

// 普通に diff を取るとわかりやすい文字列で教えてくれる
func main() {
	v1 := S1{F1: 1}
	v2 := S1{F1: 1}
	v3 := S1{F1: 2}

	if diff := cmp.Diff(v1, v2); diff != "" {
		fmt.Printf("v1 != v2\n%s\n", diff)
	} else {
		fmt.Println("v1 == v2")
	}

	if diff := cmp.Diff(v1, v3); diff != "" {
		fmt.Printf("v1 != v3\n%s\n", diff)
	} else {
		fmt.Println("v1 == v3")
	}
}

実行するこうなります。

$ go run main.go
v1 == v2
v1 != v3
{main.S1}.F1:
	-: 1
	+: 2

良いですね。

unexported なフィールドと戦う (1)

unexported なフィールドがあるとどうなるか見てみましょう。

type S2 struct {
	F1      int
	private int
}

// unexported なフィールドがある場合は AllowUnexported 等を指定しないと panic になる
func main() {
	v1 := S2{F1: 1, private: 1}
	v2 := S2{F1: 1, private: 1}
	v3 := S2{F1: 1, private: 2}

	if diff := cmp.Diff(v1, v2); diff != "" {
		fmt.Printf("v1 != v2\n%s\n", diff)
	} else {
		fmt.Println("v1 == v2")
	}

	if diff := cmp.Diff(v1, v3); diff != "" {
		fmt.Printf("v1 != v3\n%s\n", diff)
	} else {
		fmt.Println("v1 == v3")
	}
}

実行すると…

$ go run main.go
panic: cannot handle unexported field: {main.S2}.private
consider using AllowUnexported or cmpopts.IgnoreUnexported

goroutine 1 [running]:
github.com/google/go-cmp/cmp.invalid.apply(0xc420098000, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0)
	/Users/ishizuka/go/src/github.com/google/go-cmp/cmp/options.go:208 +0xf5
...

なるほど、panicになるのですね。

unexported なフィールドと戦う (2) panic良くない

panic は流石にアレなので対策しましょう。
比較時に cmp.AllowUnexported というオプションを追加すると良いです。

type S2 struct {
	F1      int
	private int
}

func main() {
	v1 := S2{F1: 1, private: 1}
	v2 := S2{F1: 1, private: 1}
	v3 := S2{F1: 1, private: 2}

	// AllowUnexported を指定して比較すると unexported な部分が違ってもdiffが出る
	opt := cmp.AllowUnexported(v1)

	if diff := cmp.Diff(v1, v2, opt); diff != "" {
		fmt.Printf("v1 != v2\n%s\n", diff)
	} else {
		fmt.Println("v1 == v2")
	}

	if diff := cmp.Diff(v1, v3, opt); diff != "" {
		fmt.Printf("v1 != v3\n%s\n", diff)
	} else {
		fmt.Println("v1 == v3")
	}
}

実行してみます。

$ go run main.go
v1 == v2
v1 != v3
{main.S2}.private:
	-: 1
	+: 2

予想通り?
unexported なフィールドのdiffも出るのですね。

unexported なフィールドと戦う (3) 無視してもらう

unexportedなフィールドは無視してくれたほうが嬉しいこともあります。
cmpopts.IgnoreUnexported を使うと無視してくれます。

type S2 struct {
	F1      int
	private int
}

func main() {
	v1 := S2{F1: 1, private: 1}
	v2 := S2{F1: 2, private: 1}
	v3 := S2{F1: 1, private: 2}
	v4 := S2{F1: 2, private: 2}

	// cmpopts.IgnoreUnexported を指定すると unexported な部分でdiffがあっても無視してくれる
	opt := cmpopts.IgnoreUnexported(v1)

	if diff := cmp.Diff(v1, v2, opt); diff != "" {
		fmt.Printf("v1 != v2\n%s\n", diff)
	} else {
		fmt.Println("v1 == v2")
	}

	if diff := cmp.Diff(v1, v3, opt); diff != "" {
		fmt.Printf("v1 != v3\n%s\n", diff)
	} else {
		fmt.Println("v1 == v3")
	}

	if diff := cmp.Diff(v1, v4, opt); diff != "" {
		fmt.Printf("v1 != v4\n%s\n", diff)
	} else {
		fmt.Println("v1 == v4")
	}
}

はい。

$ go run main.go
v1 != v2
{main.S2}.F1:
	-: 1
	+: 2

v1 == v3
v1 != v4
{main.S2}.F1:
	-: 1
	+: 2

はい。
期待通り。

いらないフィールドを無視する

ランダム値だったりパスワードのハッシュだったりが入るフィールドは無視してもらったほうがテストしやすいですよね。
そういうときは cmpopts.IgnoreFields で。

type S3 struct {
	F1 int
	F2 int
}

func main() {
	v1 := S3{F1: 1, F2: 1}
	v2 := S3{F1: 2, F2: 1}
	v3 := S3{F1: 1, F2: 2}
	v4 := S3{F1: 2, F2: 2}

	// cmpopts.IgnoreFields を指定するとそのフィールドが違っても無視してくれる
	opt := cmpopts.IgnoreFields(v1, "F2")

	if diff := cmp.Diff(v1, v2, opt); diff != "" {
		fmt.Printf("v1 != v2\n%s\n", diff)
	} else {
		fmt.Println("v1 == v2")
	}

	if diff := cmp.Diff(v1, v3, opt); diff != "" {
		fmt.Printf("v1 != v3\n%s\n", diff)
	} else {
		fmt.Println("v1 == v3")
	}

	if diff := cmp.Diff(v1, v4, opt); diff != "" {
		fmt.Printf("v1 != v4\n%s\n", diff)
	} else {
		fmt.Println("v1 == v4")
	}
}

実行しましょう。

$ go run main.go
v1 != v2
{main.S3}.F1:
	-: 1
	+: 2

v1 == v3
v1 != v4
{main.S3}.F1:
	-: 1
	+: 2

最高ですね。

ということで

go-cmp を単なるdiffがわかりやすく見えるよ、ではなくて Option を活用すると便利に使えるよ! というお話でした。
その他にもいろいろ Option があったりするので、使っていきましょう。

45
22
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?