-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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 dynamic return values #742
base: master
Are you sure you want to change the base?
Conversation
exactly this I'd need! any chance this gets looked at by a maintainer? |
This seems like valuable functionality. Any chance it will be reviewed? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I question whether using this is actually better than just putting the behaviour in a double which does not use mock.Mock?
type myMock struct {
mock.Mock
}
func (m *myMock) Method(i int) error {
return m.Called(i).Error(0)
}
func TestIfy(t *testing.T) {
clientDouble := &myMock{}
clientDouble.Test(t)
clientDouble.On("Method").Run(func(args mock.Arguments) mock.Arguments{
i := args.Int(0) // panics if myMock is wrong
if i > 10 {
return mock.Arguments{errors.New("too big")
}
return mock.Arguments{error(nil)}
}
c := &myClass{
Client: clientDouble,
}
actual := c.Do()
assert.Equal(t, "expectation", actual)
}
-- vs --
type myMock struct {}
func (m *myMock) Method(i int) error {
if i > 10 {
return errors.New("too big")
}
return nil
}
func TestIfy(t *testing.T) {
c := &myClass{
Client: &myMock{},
}
actual := c.Do()
assert.Equal(t, "expectation", actual)
}
To me, Mock
is only adding a layer of complexity. Can you show an example where this functionality is an improvement to just making your own double or mock?
mock/mock.go
Outdated
// 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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should use different method. Mock
is already not trivial to understand, each method should strive to do one thing and do it well.
IMO it's a useful feature to be able to supply your own implementation to a mocked method. https://github.com/vektra/mockery accomplishes this with generated wrappers around a testify.Mock, but wouldn't have needed a lot of the generated code if Mock had already supported this out of the box |
Two reasons I think:
|
One thing that could be nice is if the function could have the same signature as the actual method it's mocking -- and then using reflect we adapt the input Arguments into a call to it and vice versa for what it returns. (And we could panic if the signature doesn't match). OTOH that might break the API -- since some folks might indeed be wanting to assert that the method is returning some func? (Where it's incredibly unlikely that someone wants their mocked method to return |
Like I mentioned in the OP, we could leave |
I've re-implemented this using mockedGreeter.On("HelloWorld", mock.Anything).Run(func(name string) string {
return "Hello " + name
}) |
A coworker pointed out that the "hack" of assigning m.mutex.Lock()
defer m.mutex.Unlock() // protect everything below
if call.RunFn != nil {
call.RunFn(arguments)
}
returnArgs := call.ReturnArguments // maybe need to clone it to avoid the risk of the slice being mutated in place?
return returnArgs That would prevent concurrently calling the RunFn; I'm not sure the ramifications of that. |
That's what mockery's "expecter" wrappers are for, strong typing, and the RunAndReturn() method on those mirrors what you're adding directly to testify.Mock here. :) |
@dlwyatt hmm can you share how one would do the hello world example using mockery's expecter wrappers? If, as a test developer, you want to alter the implementation do you have to re-run mockery? Can different tests mock the function in different ways (eg different HelloWorld implementations)? |
I still have the same two issues with this change, one can be remedied but I don't see an answer for the other. The simple is the same comment as before: The harder one: This still wraps what would otherwise be a pure function (method) in unecessary reflection. Yes, you can hide all of the actual reflection work from the user, and it is quite pretty. But you introduce a possibility for error that the pure function does not; if the signature of the function in the |
@gburt : https://vektra.github.io/mockery/latest/features/#expecter-structs . There's a RunAndReturn example a few blocks down. No monkeypatching required. |
As for how it works, it just assigns the function itself as the "Return" value on the testify mock.Call, and the wrapper on the mockery struct checks for that type signature when it looks at the Arguments.Get(0) value. If it matches, it calls the function and returns its results. func (_c *MockSomething_DoSomething_Call) RunAndReturn(run func(string, int) (string, error)) *MockSomething_DoSomething_Call {
_c.Call.Return(run)
return _c
} // DoSomething provides a mock function with given fields: s, i
func (_m *MockSomething) DoSomething(s string, i int) (string, error) {
ret := _m.Called(s, i)
// snip
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(string, int) (string, error)); ok {
return rf(s, i)
} |
Support dynamic return arguments in a threadsafe way. (Versus setting c.ReturnValues which is shared state/not threadsafe.)
Do this by modifying
Run
to accept three types of functions:1: The status quo
Eg a
func(Arguments)
which behaves just like today; can be combined with a call toReturn()
to return fixed values2: A function which matches the signature of your mocked function itself
And determines the return values dynamically. So like a "double" but with the benefit of this library's invocation count tracking and filtering to certain input arguments. Example:
3:
func(Arguments) Arguments
which behaves like (2) except you need to do the typecasting yourselfExample:
If called with (2) or (3) then calling
Return()
as well is not allowed and will panic. IfRun()
is called with an arg that is not a function then we'll panic.Closes #350