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 があったりするので、使っていきましょう。