testify でfieldにfunc型を含むstructを assert.Equal
で比較すると、Diffにはなにも表れないがテストは失敗する。
https://github.com/stretchr/testify/issues/1146 によると、ライブラリの都合ではなく Go の仕様としてfunc型の比較はお互いがnilかどうかしかチェックできず、それ以上の比較は行えないとなっている。
funcの名前だけでも比較できればとりあえず十分かなと思ったので、名前を比較する方法を調査する。
testifyでは難しそうなので、 go-cmp を使うことにする。
普通に比較すると失敗する
import (
"testing"
"github.com/google/go-cmp/cmp"
)
type Param struct {
Condition func(x int) bool
}
func defaultCondition(x int) bool {
return x > 5
}
func NewParam() *Param {
return &Param{
Condition: defaultCondition,
}
}
func TestParam(t *testing.T) {
got := NewParam()
want := &Param{
Condition: defaultCondition,
}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Param is mismatch (-got +want):\n%s", diff)
}
}
$ go test ./
=== RUN TestParam
param_test.go:29: Param is mismatch (-got +want):
&httpclient.Param{
- Condition: ⟪0x01032d21c0⟫,
+ Condition: ⟪0x01032d21c0⟫,
}
--- FAIL: TestParam (0.00s)
FAIL
exit status 1
このように、ポインタのアドレスは同じで見た目的には差分はないのだが、diff ありとして失敗する。
assert.Equal(t, want, got)
としても同じようになる。
Error: Not equal:
expected: &Param{Condition:(func(int) bool)(0x10091e1c0)}
actual : &Param{Condition:(func(int) bool)(0x10091e1c0)}
Diff:
Test: TestParam
go-cmpのカスタムの比較処理を書く
go-cmpでカスタムの比較処理 を書いてあげれば良さそう。 Go reflectパッケージを使って関数名を取得する を応用して、以下のようにした。
import (
"reflect"
"runtime"
"testing"
"github.com/google/go-cmp/cmp"
)
type Param struct {
Name string
Body *Body
Condition func(x int) bool
Hooks []func(p *Param)
}
type Body struct {
Comment string
}
func defaultCondition(x int) bool {
return x > 5
}
func NewParam(name string, body *Body, hooks []func(p *Param)) *Param {
return &Param{
Name: name,
Body: body,
Condition: defaultCondition,
Hooks: hooks,
}
}
func TestParam(t *testing.T) {
hooks := []func(p *Param){func(p *Param) { p.Name = "MASKED" }}
opts := []cmp.Option{
cmp.FilterValues(
func(x, y any) bool {
if reflect.TypeOf(x).Kind() != reflect.TypeOf(y).Kind() {
return false
}
if reflect.TypeOf(x).Kind() == reflect.Func {
return true
}
if reflect.TypeOf(x).Kind() == reflect.Slice {
xelem := reflect.ValueOf(x)
yelem := reflect.ValueOf(y)
if xelem.Len() > 0 && yelem.Len() > 0 {
return reflect.TypeOf(xelem.Index(0)).Kind() == reflect.Func &&
reflect.TypeOf(yelem.Index(0)).Kind() == reflect.Func
}
}
return false
},
cmp.Transformer("FuncName", func(i any) string {
if reflect.TypeOf(i).Kind() == reflect.Func {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
} else if reflect.TypeOf(i).Kind() == reflect.Slice {
sliceVal := reflect.ValueOf(i)
if sliceVal.Len() > 0 {
firstElem := sliceVal.Index(0)
return runtime.FuncForPC(firstElem.Pointer()).Name()
}
}
return ""
}),
),
}
got := NewParam("the name", &Body{Comment: "foo"}, hooks)
want := &Param{
Name: "the name",
Body: &Body{
Comment: "foo",
},
Condition: defaultCondition,
Hooks: hooks,
}
if diff := cmp.Diff(got, want, opts...); diff != "" {
t.Errorf("Param is mismatch (-got +want):\n%s", diff)
}
}
これで go test を実行するとテストが通るようになった
$ go test ./
=== RUN TestParam
--- PASS: TestParam (0.00s)
PASS
ok
改良
もう少し調べたら、sliceの中身はgo-cmpの方で展開してくれるので、Kind() == reflect.Func
の場合のみTransformを行っても問題なかった。
import (
"reflect"
"runtime"
"testing"
"github.com/google/go-cmp/cmp"
)
type Param struct {
Name string
Body *Body
Condition func(x int) bool
Hooks []func(p *Param)
}
type Body struct {
Comment string
}
func defaultCondition(x int) bool {
return x > 5
}
func NewParam(name string, body *Body, hooks []func(p *Param)) *Param {
return &Param{
Name: name,
Body: body,
Condition: defaultCondition,
Hooks: hooks,
}
}
func TestParam(t *testing.T) {
hooks := []func(p *Param){
func(p *Param) { p.Name = "MASKED" },
func(p *Param) { p.Body.Comment = "MASKED" },
}
opts := []cmp.Option{
cmp.FilterValues(
func(x, y any) bool {
return reflect.TypeOf(x).Kind() == reflect.TypeOf(y).Kind() && reflect.TypeOf(x).Kind() == reflect.Func
},
cmp.Transformer("FuncName", func(i any) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}),
),
}
got := NewParam("the name", &Body{Comment: "foo"}, hooks)
want := &Param{
Name: "the name",
Body: &Body{
Comment: "foo",
},
Condition: defaultCondition,
Hooks: hooks,
}
if diff := cmp.Diff(got, want, opts...); diff != "" {
t.Errorf("Param is mismatch (-got +want):\n%s", diff)
}
}
一応、ちゃんと FuncName
が実行されているか確認したところちゃんと通っていた。
// ....
cmp.Transformer("FuncName", func(i any) string {
funcName := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
fmt.Printf("funcName=%s\n", funcName)
return funcName
}),
// ....
❯ go test -v -run TestParam$
=== RUN TestParam
funcName=defaultCondition
funcName=defaultCondition
funcName=TestParam.func2
funcName=TestParam.func2
funcName=TestParam.func1
funcName=TestParam.func1
funcName=TestParam.func1
funcName=TestParam.func1
funcName=TestParam.func1
funcName=TestParam.func2
funcName=TestParam.func2
--- PASS: TestParam (0.00s)
PASS