Skip to content

Commit

Permalink
Support optional diff formatter fn for eq
Browse files Browse the repository at this point in the history
  • Loading branch information
orls committed Oct 27, 2024
1 parent b8222fa commit d5b543c
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 18 deletions.
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

0 comments on commit d5b543c

Please sign in to comment.