Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support optional diff formatter fn for eq #221

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,39 @@ Got: [0 1 1 2 3]
Want: is equal to 1
```

### Custom diff formatter for unequal args

You can provide a custom diff formatter function to the Controller, which will
be invoked instead of the default formatting when the `Eq` matcher fails.

(`Eq` is the default matcher for expectations with arbitrary values).

Other matchers are unaffected. (This includes `GotFormatter` implementations
and matchers wrapped in `WantFormatter` documented below).

```go
myDiffer := func(expected, actual any) {
return fmt.Sprintf("My custom diff:\n- %v\n+ %v", expected, actual)
// or pass to another lib, like go-cmp cmp.Diff
}

ctrl := gomock.NewController(t, gomock.WithDiffFormatter(myDiffer))

// ...

mymock.EXPECT().Foo("my expected string")
mymock.Foo("my actual string")
```

```
Unexpected call to *mymocks.MyMock.Foo([my actual string]) at ... because:
expected call at ... doesn't match the argument at index 0.
My custom diff:
- my expected string
+ my actual string
```


### Modifying `Want`

The `Want` value comes from the matcher's `String()` method. If the matcher's
Expand Down
31 changes: 20 additions & 11 deletions gomock/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ type Call struct {
// can set the return values by returning a non-nil slice. Actions run in the
// order they are created.
actions []func([]any) []any

fmtDiff DiffFormatter
}

// newCall creates a *Call. It requires the method type in order to support
// unexported methods.
func newCall(t TestHelper, receiver any, method string, methodType reflect.Type, args ...any) *Call {
func newCall(t TestHelper, fmtDiff DiffFormatter, receiver any, method string, methodType reflect.Type, args ...any) *Call {
t.Helper()

// TODO: check arity, types.
Expand Down Expand Up @@ -78,6 +80,7 @@ func newCall(t TestHelper, receiver any, method string, methodType reflect.Type,
return &Call{
t: t, receiver: receiver, method: method, methodType: methodType,
args: mArgs, origin: origin, minCalls: 1, maxCalls: 1, actions: actions,
fmtDiff: fmtDiff,
}
}

Expand Down Expand Up @@ -331,8 +334,8 @@ func (c *Call) matches(args []any) error {
for i, m := range c.args {
if !m.Matches(args[i]) {
return fmt.Errorf(
"expected call at %s doesn't match the argument at index %d.\nGot: %v\nWant: %v",
c.origin, i, formatGottenArg(m, args[i]), m,
"expected call at %s doesn't match the argument at index %d.\n%s",
c.origin, i, c.formatArgMismatch(m, args[i]),
)
}
}
Expand All @@ -354,8 +357,9 @@ func (c *Call) matches(args []any) error {
if i < c.methodType.NumIn()-1 {
// Non-variadic args
if !m.Matches(args[i]) {
return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v",
c.origin, strconv.Itoa(i), formatGottenArg(m, args[i]), m)
return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\n%s",
c.origin, strconv.Itoa(i), c.formatArgMismatch(m, args[i]),
)
}
continue
}
Expand Down Expand Up @@ -398,8 +402,9 @@ func (c *Call) matches(args []any) error {
// Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, matcherC, matcherD)
// Got Foo(a, b, c) want Foo(matcherA, matcherB)

return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v",
c.origin, strconv.Itoa(i), formatGottenArg(m, args[i:]), c.args[i])
return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\n%s",
c.origin, strconv.Itoa(i), c.formatArgMismatch(m, args[i:]),
)
}
}

Expand Down Expand Up @@ -497,10 +502,14 @@ func (c *Call) addAction(action func([]any) []any) {
c.actions = append(c.actions, action)
}

func formatGottenArg(m Matcher, arg any) string {
got := fmt.Sprintf("%v (%T)", arg, arg)
func (c *Call) formatArgMismatch(m Matcher, actual any) string {
if eqm, ok := m.(eqMatcher); ok && c.fmtDiff != nil {
return c.fmtDiff(eqm.x, actual)
}

got := fmt.Sprintf("%v (%T)", actual, actual)
if gs, ok := m.(GotFormatter); ok {
got = gs.Got(arg)
got = gs.Got(actual)
}
return got
return fmt.Sprintf("Got: %v\nWant: %v", got, m.String())
}
8 changes: 4 additions & 4 deletions gomock/callset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestCallSetAdd(t *testing.T) {

numCalls := 10
for i := 0; i < numCalls; i++ {
cs.Add(newCall(t, receiver, method, reflect.TypeOf(receiverType{}.Func)))
cs.Add(newCall(t, nil, receiver, method, reflect.TypeOf(receiverType{}.Func)))
}

call, err := cs.FindMatch(receiver, method, []any{})
Expand All @@ -47,13 +47,13 @@ func TestCallSetAdd_WhenOverridable_ClearsPreviousExpectedAndExhausted(t *testin
var receiver any = "TestReceiver"
cs := newOverridableCallSet()

cs.Add(newCall(t, receiver, method, reflect.TypeOf(receiverType{}.Func)))
cs.Add(newCall(t, nil, receiver, method, reflect.TypeOf(receiverType{}.Func)))
numExpectedCalls := len(cs.expected[callSetKey{receiver, method}])
if numExpectedCalls != 1 {
t.Fatalf("Expected 1 expected call in callset, got %d", numExpectedCalls)
}

cs.Add(newCall(t, receiver, method, reflect.TypeOf(receiverType{}.Func)))
cs.Add(newCall(t, nil, receiver, method, reflect.TypeOf(receiverType{}.Func)))
newNumExpectedCalls := len(cs.expected[callSetKey{receiver, method}])
if newNumExpectedCalls != 1 {
t.Fatalf("Expected 1 expected call in callset, got %d", newNumExpectedCalls)
Expand Down Expand Up @@ -100,7 +100,7 @@ func TestCallSetFindMatch(t *testing.T) {
method := "TestMethod"
args := []any{}

c1 := newCall(t, receiver, method, reflect.TypeOf(receiverType{}.Func))
c1 := newCall(t, nil, receiver, method, reflect.TypeOf(receiverType{}.Func))
cs.exhausted = map[callSetKey][]*Call{
{receiver: receiver, fname: method}: {c1},
}
Expand Down
21 changes: 20 additions & 1 deletion gomock/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ type TestHelper interface {
Helper()
}

// DiffFormatter is a function to print custom diffs. See WithDiffFormatter.
type DiffFormatter func(expected, actual any) string

// cleanuper is used to check if TestHelper also has the `Cleanup` method. A
// common pattern is to pass in a `*testing.T` to
// `NewController(t TestReporter)`. In Go 1.14+, `*testing.T` has a cleanup
Expand Down Expand Up @@ -75,6 +78,7 @@ type Controller struct {
mu sync.Mutex
expectedCalls *callSet
finished bool
fmtDiff DiffFormatter
}

// NewController returns a new Controller. It is the preferred way to create a Controller.
Expand Down Expand Up @@ -120,6 +124,21 @@ func (o overridableExpectationsOption) apply(ctrl *Controller) {
ctrl.expectedCalls = newOverridableCallSet()
}

type fmtDiffOption struct {
fmtDiff DiffFormatter
}

// WithDiffFormatter allows customizing output format when args to a call don't
// match expectations. Note that this only applies when the default equality
// matcher is being used.
func WithDiffFormatter(fmtDiff DiffFormatter) fmtDiffOption {
return fmtDiffOption{fmtDiff: fmtDiff}
}

func (o fmtDiffOption) apply(ctrl *Controller) {
ctrl.fmtDiff = o.fmtDiff
}

type cancelReporter struct {
t TestHelper
cancel func()
Expand Down Expand Up @@ -182,7 +201,7 @@ func (ctrl *Controller) RecordCall(receiver any, method string, args ...any) *Ca
func (ctrl *Controller) RecordCallWithMethodType(receiver any, method string, methodType reflect.Type, args ...any) *Call {
ctrl.T.Helper()

call := newCall(ctrl.T, receiver, method, methodType, args...)
call := newCall(ctrl.T, ctrl.fmtDiff, receiver, method, methodType, args...)

ctrl.mu.Lock()
defer ctrl.mu.Unlock()
Expand Down
114 changes: 112 additions & 2 deletions gomock/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,12 @@ func assertEqual(t *testing.T, expected any, actual any) {
}
}

func createFixtures(t *testing.T) (reporter *ErrorReporter, ctrl *gomock.Controller) {
func createFixtures(t *testing.T, opts ...gomock.ControllerOption) (reporter *ErrorReporter, ctrl *gomock.Controller) {
// reporter acts as a testing.T-like object that we pass to the
// Controller. We use it to test that the mock considered tests
// successful or failed.
reporter = NewErrorReporter(t)
ctrl = gomock.NewController(reporter)
ctrl = gomock.NewController(reporter, opts...)
return
}

Expand Down Expand Up @@ -817,6 +817,116 @@ func TestVariadicArgumentsGotFormatterTooManyArgsFailure(t *testing.T) {
ctrl.Call(s, "VariadicMethod", 0, "1")
}

func TestCustomDiff(t *testing.T) {

diff := func(expected, actual any) string {
return fmt.Sprintf("EXPECT{%v} ACTUAL{%v}", expected, actual)
}
rep, ctrl := createFixtures(t, gomock.WithDiffFormatter(diff))
defer rep.recoverUnexpectedFatal()

s := new(Subject)
ctrl.RecordCall(
s,
"FooMethod",
gomock.Eq("aaa"),
)

rep.assertFatal(func() {
ctrl.Call(s, "FooMethod", "bbb")
}, "expected call to", "doesn't match the argument at index 0",
"EXPECT{aaa} ACTUAL{bbb}")
}

func TestCustomDiff_RawExpectValue(t *testing.T) {

diff := func(expected, actual any) string {
return fmt.Sprintf("EXPECT{%v} ACTUAL{%v}", expected, actual)
}
rep, ctrl := createFixtures(t, gomock.WithDiffFormatter(diff))
defer rep.recoverUnexpectedFatal()

s := new(Subject)
ctrl.RecordCall(
s,
"FooMethod",
"aaa",
)

rep.assertFatal(func() {
ctrl.Call(s, "FooMethod", "bbb")
}, "expected call to", "doesn't match the argument at index 0",
"EXPECT{aaa} ACTUAL{bbb}")
}

func TestCustomDiff_defersToGotFormatter(t *testing.T) {
diff := func(expected, actual any) string {
return "this should lose"
}
rep, ctrl := createFixtures(t, gomock.WithDiffFormatter(diff))
defer rep.recoverUnexpectedFatal()

s := new(Subject)
ctrl.RecordCall(
s,
"FooMethod",
gomock.GotFormatterAdapter(
gomock.GotFormatterFunc(func(got any) string { return "this should win" }),
gomock.Eq("aaa"),
),
)

rep.assertFatal(func() {
ctrl.Call(s, "FooMethod", "bbb")
}, "expected call to", "doesn't match the argument at index 0",
"Got: this should win\nWant:")
}

func TestCustomDiff_defersToWantFormatter(t *testing.T) {
diff := func(expected, actual any) string {
return "this should lose"
}
rep, ctrl := createFixtures(t, gomock.WithDiffFormatter(diff))
defer rep.recoverUnexpectedFatal()

s := new(Subject)
ctrl.RecordCall(
s,
"FooMethod",
gomock.WantFormatter(
gomock.StringerFunc(func() string { return "this should win" }),
gomock.Eq("aaa"),
),
)

rep.assertFatal(func() {
ctrl.Call(s, "FooMethod", "bbb")
}, "expected call to", "doesn't match the argument at index 0",
"Got: bbb (string)\nWant: this should win")
}

func TestCustomDiff_WithVariadicArguments(t *testing.T) {
diff := func(expected, actual any) string {
return "this should lose"
}
rep, ctrl := createFixtures(t, gomock.WithDiffFormatter(diff))
defer rep.recoverUnexpectedFatal()

s := new(Subject)
ctrl.RecordCall(
s,
"VariadicMethod",
0,
"1",
)

rep.assertFatal(func() {
ctrl.Call(s, "VariadicMethod", 0, "2", "3")
}, "expected call to", "doesn't match the argument at index 1",
"Got: [2 3] ([]interface {})\nWant: this should appear")
ctrl.Call(s, "VariadicMethod", 0, "1")
}

func TestNoHelper(t *testing.T) {
ctrlNoHelper := gomock.NewController(NewErrorReporter(t))

Expand Down