From 06016efbb13fd8fdd12cfe201eeaf042cea7b822 Mon Sep 17 00:00:00 2001 From: Gabriel Burt Date: Fri, 22 Mar 2019 19:21:22 +0000 Subject: [PATCH 1/6] allow mocked.On("X").Return(func(a Arguments) Arguments { return nil }) --- mock/mock.go | 41 +++++++++++++++++++++++++++++++++++++++-- mock/mock_test.go | 15 +++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/mock/mock.go b/mock/mock.go index d6694ed78..d34311566 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -27,6 +27,8 @@ type TestingT interface { Call */ +type returnArgumentsFunc func(args Arguments) Arguments + // Call represents a method call and is used for setting expectations, // as well as recording activity. type Call struct { @@ -39,9 +41,15 @@ type Call struct { Arguments Arguments // Holds the arguments that should be returned when - // this method is called. + // this method is called. If the first and only value is + // function which takes and returns Arguments, that will be invoked + // on each call of the mock to determine what to return. ReturnArguments Arguments + // if the first arg in ReturnArguments is a returnArgumentsFunc, this + // stores that ready for use + returnFunc returnArgumentsFunc + // Holds the caller info for the On() call callerInfo []string @@ -88,15 +96,44 @@ func (c *Call) unlock() { c.Parent.mutex.Unlock() } +// If the only return arg is a function which takes and returns Arguments, invoke it instead of returning it as the value +func (c *Call) getReturnArguments(args Arguments) Arguments { + if c.returnFunc != nil { + return c.returnFunc(args) + } + + return c.ReturnArguments +} + +var argumentsType = reflect.TypeOf(Arguments(nil)) + // Return specifies the return arguments for the expectation. // // Mock.On("DoSomething").Return(errors.New("failed")) +// +// If you pass a single returnArg which is a function that itself takes Arguments and returns Arguments, +// that will be invoked at runtime. +// +// Mock.On("HelloWorld").Return(func(args mock.Arguments) mock.Arguments { return "Hello " + arg[0].(string) }) func (c *Call) Return(returnArguments ...interface{}) *Call { c.lock() defer c.unlock() c.ReturnArguments = returnArguments + if len(c.ReturnArguments) == 1 { + fn := reflect.ValueOf(c.ReturnArguments[0]) + if fn.Kind() == reflect.Func { + fnType := fn.Type() + if fnType.NumIn() == 1 && fnType.NumOut() == 1 && fnType.In(0) == argumentsType && fnType.Out(0) == argumentsType { + c.returnFunc = func(args Arguments) Arguments { + ret := fn.Call([]reflect.Value{reflect.ValueOf(args)}) + return ret[0].Interface().(Arguments) + } + } + } + } + return c } @@ -393,7 +430,7 @@ func (m *Mock) MethodCalled(methodName string, arguments ...interface{}) Argumen } m.mutex.Lock() - returnArgs := call.ReturnArguments + returnArgs := call.getReturnArguments(arguments) m.mutex.Unlock() return returnArgs diff --git a/mock/mock_test.go b/mock/mock_test.go index 2608f5a36..b5586c87c 100644 --- a/mock/mock_test.go +++ b/mock/mock_test.go @@ -596,6 +596,21 @@ func Test_Mock_Return_Run_Out_Of_Order(t *testing.T) { assert.NotNil(t, call.Run) } +func Test_Mock_Return_Func(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + mockedService.On("TheExampleMethod", 1, 2, 3). + Return(func(args Arguments) Arguments { + return []interface{}{42, fmt.Errorf("hrm")} + }). + Once() + + answer, _ := mockedService.TheExampleMethod(1, 2, 3) + assert.Equal(t, 42, answer) +} + func Test_Mock_Return_Once(t *testing.T) { // make a test impl object From cc10451e13ac2626b56f233aa3b1fe721d9dfd49 Mon Sep 17 00:00:00 2001 From: Gabriel Burt Date: Wed, 6 Mar 2024 14:43:05 +0000 Subject: [PATCH 2/6] better mock ReturnFunc test --- mock/mock_test.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mock/mock_test.go b/mock/mock_test.go index de81845be..390d1f727 100644 --- a/mock/mock_test.go +++ b/mock/mock_test.go @@ -828,14 +828,17 @@ func Test_Mock_Return_Func(t *testing.T) { // make a test impl object var mockedService = new(TestExampleImplementation) - mockedService.On("TheExampleMethod", 1, 2, 3). + mockedService.On("TheExampleMethod", Anything, Anything, Anything). Return(func(args Arguments) Arguments { - return []interface{}{42, fmt.Errorf("hrm")} + return Arguments{args[0].(int) + 40, fmt.Errorf("hrm")} }). - Once() + Twice() - answer, _ := mockedService.TheExampleMethod(1, 2, 3) + answer, _ := mockedService.TheExampleMethod(2, 4, 5) assert.Equal(t, 42, answer) + + answer, _ = mockedService.TheExampleMethod(44, 4, 5) + assert.Equal(t, 84, answer) } func Test_Mock_Return_Once(t *testing.T) { From da297380e0eb87e0da74ca365877a64536eaceda Mon Sep 17 00:00:00 2001 From: Gabriel Burt Date: Wed, 6 Mar 2024 14:51:29 +0000 Subject: [PATCH 3/6] simpler implementation per review feedback --- mock/mock.go | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/mock/mock.go b/mock/mock.go index cc6817061..95f2b71c7 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -32,8 +32,6 @@ type TestingT interface { Call */ -type returnArgumentsFunc func(args Arguments) Arguments - // Call represents a method call and is used for setting expectations, // as well as recording activity. type Call struct { @@ -51,9 +49,9 @@ type Call struct { // on each call of the mock to determine what to return. ReturnArguments Arguments - // if the first arg in ReturnArguments is a returnArgumentsFunc, this + // if the first arg in ReturnArguments is a func(args Arguments) Arguments, this // stores that ready for use - returnFunc returnArgumentsFunc + returnFunc func(args Arguments) Arguments // Holds the caller info for the On() call callerInfo []string @@ -135,21 +133,15 @@ func (c *Call) Return(returnArguments ...interface{}) *Call { c.lock() defer c.unlock() - c.ReturnArguments = returnArguments - - if len(c.ReturnArguments) == 1 { - fn := reflect.ValueOf(c.ReturnArguments[0]) - if fn.Kind() == reflect.Func { - fnType := fn.Type() - if fnType.NumIn() == 1 && fnType.NumOut() == 1 && fnType.In(0) == argumentsType && fnType.Out(0) == argumentsType { - c.returnFunc = func(args Arguments) Arguments { - ret := fn.Call([]reflect.Value{reflect.ValueOf(args)}) - return ret[0].Interface().(Arguments) - } - } + if len(returnArguments) == 1 { + if fn, ok := returnArguments[0].(func(Arguments) Arguments); ok { + c.returnFunc = fn + return c } } + c.returnFunc = nil + c.ReturnArguments = returnArguments return c } From 9cacd192f23cb74d0a31b784d15ecb4efcb7c2f1 Mon Sep 17 00:00:00 2001 From: Gabriel Burt Date: Wed, 6 Mar 2024 14:53:20 +0000 Subject: [PATCH 4/6] remove unused variable --- mock/mock.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/mock/mock.go b/mock/mock.go index 95f2b71c7..44ccedbda 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -117,8 +117,6 @@ func (c *Call) getReturnArguments(args Arguments) Arguments { return c.ReturnArguments } -var argumentsType = reflect.TypeOf(Arguments(nil)) - // Return specifies the return arguments for the expectation. // // Mock.On("DoSomething").Return(errors.New("failed")) From 944faeee5fec53b9d2dc936af5ff207c17f63076 Mon Sep 17 00:00:00 2001 From: Gabriel Burt Date: Thu, 7 Mar 2024 16:10:02 +0000 Subject: [PATCH 5/6] reimplement via Run() func --- mock/mock.go | 88 ++++++++++++++++++++++++++++++++------------- mock/mock_test.go | 91 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 144 insertions(+), 35 deletions(-) diff --git a/mock/mock.go b/mock/mock.go index 44ccedbda..b1094545c 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -49,8 +49,8 @@ type Call struct { // on each call of the mock to determine what to return. ReturnArguments Arguments - // if the first arg in ReturnArguments is a func(args Arguments) Arguments, this - // stores that ready for use + // if Run() was given a function which returns arguments, we'll call that whenever + // this call is invoked and use its return values as the arguments to return. returnFunc func(args Arguments) Arguments // Holds the caller info for the On() call @@ -110,6 +110,10 @@ func (c *Call) unlock() { // If the only return arg is a function which takes and returns Arguments, invoke it instead of returning it as the value func (c *Call) getReturnArguments(args Arguments) Arguments { + if c.returnFunc != nil && len(c.ReturnArguments) > 0 { + panic("Cannot specify a function with Run() that returns arguments and also specify a Return() fixed set of return arguments") + } + if c.returnFunc != nil { return c.returnFunc(args) } @@ -117,28 +121,13 @@ func (c *Call) getReturnArguments(args Arguments) Arguments { return c.ReturnArguments } -// Return specifies the return arguments for the expectation. +// Return specifies fixed return arguments for the expectation, that will be returned for every invocation. +// If you want to specify dynamic return values see the Run(fn) function. // // Mock.On("DoSomething").Return(errors.New("failed")) -// -// If you pass a single returnArg which is a function that itself takes Arguments and returns Arguments, -// that will be invoked at runtime, letting you dynamically return different values. -// -// Mock.On("HelloWorld").Return(func(args mock.Arguments) mock.Arguments { -// return Arguments{"Hello " + arg[0].(string)} -// }) func (c *Call) Return(returnArguments ...interface{}) *Call { c.lock() defer c.unlock() - - if len(returnArguments) == 1 { - if fn, ok := returnArguments[0].(func(Arguments) Arguments); ok { - c.returnFunc = fn - return c - } - } - - c.returnFunc = nil c.ReturnArguments = returnArguments return c } @@ -201,18 +190,67 @@ func (c *Call) After(d time.Duration) *Call { return c } -// Run sets a handler to be called before returning. It can be used when -// mocking a method (such as an unmarshaler) that takes a pointer to a struct and -// sets properties in such struct +// Run sets a handler to be called before returning, possibly determining the return values of the call too. +// +// You can pass three types of functions to it: // -// Mock.On("Unmarshal", AnythingOfType("*map[string]interface{}")).Return().Run(func(args Arguments) { +// 1) func(Arguments) that will not affect what is returned (you can still call Return() to specify them) +// +// Mock.On("Unmarshal", mock.AnythingOfType("*map[string]interface{}")).Return().Run(func(args Arguments) { // arg := args.Get(0).(*map[string]interface{}) // arg["foo"] = "bar" // }) -func (c *Call) Run(fn func(args Arguments)) *Call { +// +// 2) A function which matches the signature of your mocked function itself, and determines the return values dynamically. +// +// Mock.On("HelloWorld", mock.Anything).Run(func(name string) string { +// return "Hello " + name +// }) +// +// 3) func(Arguments) Arguments which behaves like (2) except you need to do the typecasting yourself +// +// Mock.On("HelloWorld", mock.Anything).Run(func(args mock.Arguments) args mock.Arguments { +// return mock.Arguments([]any{"Hello " + args[0].(string)}) +// }) +func (c *Call) Run(fn interface{}) *Call { c.lock() defer c.unlock() - c.RunFn = fn + switch f := fn.(type) { + case func(Arguments): + c.RunFn = f + case func(Arguments) Arguments: + c.returnFunc = f + default: + fnVal := reflect.ValueOf(fn) + if fnVal.Kind() != reflect.Func { + panic(fmt.Sprintf("Invalid argument passed to Run(), must be a function, is a %T", fn)) + } + fnType := fnVal.Type() + c.returnFunc = func(args Arguments) (resp Arguments) { + var argVals []reflect.Value + for i, arg := range args { + if i == len(args)-1 && fnType.IsVariadic() { + // splat the variadic arg back out in the call, as expected by reflect.Value#Call + argVal := reflect.ValueOf(arg) + for j := 0; j < argVal.Len(); j++ { + argVals = append(argVals, argVal.Index(j)) + } + } else { + argVals = append(argVals, reflect.ValueOf(arg)) + } + } + + // actually call the fn + ret := fnVal.Call(argVals) + + for _, val := range ret { + resp = append(resp, val.Interface()) + } + + return resp + } + } + return c } diff --git a/mock/mock_test.go b/mock/mock_test.go index 390d1f727..078914414 100644 --- a/mock/mock_test.go +++ b/mock/mock_test.go @@ -823,22 +823,93 @@ func Test_Mock_Return_Run_Out_Of_Order(t *testing.T) { assert.NotNil(t, call.Run) } -func Test_Mock_Return_Func(t *testing.T) { +func Test_Mock_Run_ReturnFunc(t *testing.T) { // make a test impl object var mockedService = new(TestExampleImplementation) - mockedService.On("TheExampleMethod", Anything, Anything, Anything). - Return(func(args Arguments) Arguments { - return Arguments{args[0].(int) + 40, fmt.Errorf("hrm")} - }). - Twice() + t.Run("can dynamically set the return values", func(t *testing.T) { + mockedService.On("TheExampleMethod", Anything, Anything, Anything). + Run(func(a, b, c int) (int, error) { + return a + 40, fmt.Errorf("hmm") + }). + Twice() + + answer, _ := mockedService.TheExampleMethod(2, 4, 5) + assert.Equal(t, 42, answer) + + answer, _ = mockedService.TheExampleMethod(44, 4, 5) + assert.Equal(t, 84, answer) + }) + + t.Run("handles func(Args) Args style", func(t *testing.T) { + mockedService.On("TheExampleMethod", Anything, Anything, Anything). + Run(func(args Arguments) Arguments { + return []interface{}{args[0].(int) + 40, fmt.Errorf("hmm")} + }). + Twice() + + answer, _ := mockedService.TheExampleMethod(2, 4, 5) + assert.Equal(t, 42, answer) + + answer, _ = mockedService.TheExampleMethod(44, 4, 5) + assert.Equal(t, 84, answer) + }) + + t.Run("handles pointer input args", func(t *testing.T) { + mockedService.On("TheExampleMethod3", Anything).Run(func(et *ExampleType) error { + if et == nil { + return fmt.Errorf("Nil obj") + } + return nil + }).Twice() + + err := mockedService.TheExampleMethod3(nil) + assert.Error(t, err) + + err = mockedService.TheExampleMethod3(&ExampleType{}) + assert.NoError(t, err) + }) + + t.Run("handles no return args", func(t *testing.T) { + mockedService.On("TheExampleMethod2", Anything).Run(func(yesno bool) { + // nothing to return + }).Once() - answer, _ := mockedService.TheExampleMethod(2, 4, 5) - assert.Equal(t, 42, answer) + mockedService.TheExampleMethod2(true) + }) - answer, _ = mockedService.TheExampleMethod(44, 4, 5) - assert.Equal(t, 84, answer) + t.Run("handles variadic input args", func(t *testing.T) { + mockedService. + On("TheExampleMethodMixedVariadic", Anything, Anything). + Run(func(a int, b ...int) error { + var sum = a + for _, v := range b { + sum += v + } + return fmt.Errorf("%v", sum) + }) + + assert.Equal(t, "42", mockedService.TheExampleMethodMixedVariadic(40, 1, 1).Error()) + assert.Equal(t, "40", mockedService.TheExampleMethodMixedVariadic(40).Error()) + }) + + t.Run("panics if Run() called with an invalid value", func(t *testing.T) { + assert.PanicsWithValue(t, + "Invalid argument passed to Run(), must be a function, is a int", + func() { mockedService.On("TheExampleMethod").Run(42) }, + ) + }) + + t.Run("panics if both Return() and Run() are called specifying return args", func(t *testing.T) { + mockedService.On("TheExampleMethod", Anything, Anything, Anything). + Run(func(a, b, c int) (int, error) { + return a + 40, fmt.Errorf("hmm") + }). + Return(80, nil) + + assert.PanicsWithValue(t, "Cannot specify a function with Run() that returns arguments and also specify a Return() fixed set of return arguments", func() { mockedService.TheExampleMethod(1, 2, 3) }) + }) } func Test_Mock_Return_Once(t *testing.T) { From ac743796a8b5b31a28d898220cfbcea48a2a0a4e Mon Sep 17 00:00:00 2001 From: Gabriel Burt Date: Thu, 7 Mar 2024 16:11:30 +0000 Subject: [PATCH 6/6] fix typo in comment --- mock/mock.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mock/mock.go b/mock/mock.go index b1094545c..b13d6ea08 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -209,7 +209,7 @@ func (c *Call) After(d time.Duration) *Call { // // 3) func(Arguments) Arguments which behaves like (2) except you need to do the typecasting yourself // -// Mock.On("HelloWorld", mock.Anything).Run(func(args mock.Arguments) args mock.Arguments { +// Mock.On("HelloWorld", mock.Anything).Run(func(args mock.Arguments) mock.Arguments { // return mock.Arguments([]any{"Hello " + args[0].(string)}) // }) func (c *Call) Run(fn interface{}) *Call {