testify
testify copied to clipboard
Allow dynamic returns based on arguments
C# Moq allows to return data based on call arguments.
testify/mock
don't. It's possible to do using Run
, but it's a big mess of code.
It would be great to have something like this:
myMock.On("Load", mock.AnythingOfType("string")).ReturnFn(func (token string) (*MyObj, error) {
if isValid(token) {
return someStuff(), nil
} else {
return nil, errors.New("Oh!")
}
})
I can send a PR if someone like this idea.
That example would not build since the func isn't returning anything.
Also, you can do:
myMock.On("Load", mock.MatchedBy(func (token string) bool { return !isValid(token) }).Return(nil, errors.New("invalid"))
@ernesto-jimenez I fixed the example.
I'll try your example.
@ernesto-jimenez the thing is, I need to return some value based on input.
I could use On
with multiple times with multiple values. But my problem is when I don't know the expected param value. Assuming the example, token
is a random UUID, it changes every call.
For this case there's almost no good way to return data based on input.
@alexandrevicenzi I'm struggling to see the use case. Why do you need some logic within that method?
If you are worried about asserting the mock is getting a valid token, you could do:
myMock.On("Load", mock.AnythingOfType("string")).Run(func(args mock.Arguments) { {
assert.Equal(true, isValid(args.Get(0).(string)), "valid token expected")
})
Our mocks package currently cares only about inputs and outputs, rather than building custom outputs based on your own logic. If you want to have more complex mocks, you can build them easily and even leverage this package. Here is a quick example:
type yourCustomStruct struct {
yourMock
}
func (s *yourCustomStruct) Load(token string) (*MyObj, error) {
s.yourMock.Called(token) // <- this is assuming you want to do myMock.On("Load", ...) and then assert it has been called
if isValid(token) {
return someStuff(), nil
} else {
return nil, errors.New("Oh!")
}
}
yourCustomMock
will have all the methods from yourMock
through promotion, but Load
will have some special logic. It has the extra bonus that you can reuse it across many tests instead of defining it within each test with myMock.On
.
@ernesto-jimenez Yes, this works and it's reusable. But what if this logic must change in some scenario?
I know that I could create many On("Something")
and return data based in the input provided in the arguments.
I was just thinking that this feature could be a good hack somewhere, for me it would be more easy to make a quick setup rather than creating multiples On("Something")
.
if you want different behaviors you can have different structs, or you could have the struct take a function:
type yourCustomStruct struct {
yourMock
loadFn func(string) (*MyObj, error)
}
func (s *yourCustomStruct) Load(token string) (*MyObj, error) {
s.yourMock.Called(token) // <- this is assuming you want to do myMock.On("Load", ...) and then assert it has been called
return s.loadFn(token)
}
@alexandrevicenzi looks like it's solved here https://github.com/vektra/mockery#return-value-provider-functions
@alexandrevicenzi @ernesto-jimenez I am trying to mock a file system like object on which I want to return some data on first call and return EOF on second call. Now to achieve this I have to write custom function in the mock object to track the no. of function calls which is already done by mock
. If mock
provides a way to call custom function passing all the information it has collected then it would be super useful.
In addition to the above reasons, I'd like to see this feature added so that I can mock an API returning a Reader
that is expected to be invoked multiple times:
httpClient := new(mockHttpClient)
httpClient.On(
"Do",
mock.MatchedBy(func(req *http.Request) bool {
// Do some logic to accept the request
return true
}
).Return(
&http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(strings.NewReader("response data")),
},
nil,
).Times(10)
The problem here is that the Reader
works just fine for the first invocation, but the second invocation is given the same Reader
which is now at end-of-input. If there were a way to return a new Reader
upon each invocation, this wouldn't be a problem.
Let's assume that the Times
expectation is a large enough number that hand-rolling separate call expectations that return different response objects the "static way" is undesirable.
Following @ernesto-jimenez's workaround is sufficient, but a baked-in solution would be nice.
Almost any other mocking framework (even some golang ones) allows for this, what is the problem of having it also in this one?
I am also stuck here, I am trying to test a function which tries to push to the queue and tries 5 times, I want the mocked queue to return error first 3 times and success just after that. how can we achieve this with current implementation?
This snippet is slightly hacky, but works in existing testify API:
m := &mocks.UploadGetter{}
m.On("Get", mock.Anything).Run(func(args mock.Arguments) {
for _, c := range m.ExpectedCalls {
if c.Method == "Get" {
c.ReturnArguments = mock.Arguments{uploadFromId(args[0].(string)), nil}
}
}
})
this was supported by testify already, it's RunFn https://github.com/stretchr/testify/blob/858f37ff9bc48070cde7f2c2895dbe0db1ad9326/mock/mock.go#L67
sample code
mockCall := mock.On("methodName", mock.Anything, mock.Anything)
mockCall.RunFn = func(args mock.Arguments) {
code := args[1].(string)
...
mockCall.ReturnArguments = mock.Arguments{nil, nil}
}
}
@hieuvo I might have missed something, but this isn't safe for concurrent code, as it isn't changing the return for the function instance, it is changing the return for the mock?
@andreib1 Right, this is not concurrent safe, moreover, it is not an officially recommended solution but rather a hackish way one can use as long a we don't have a nice way to mock return values.
It's frustrating this hasn't been officially implemented, as it is a real pain to mock tests that fail for intermittent calls. Examples where this is needed are filesystem and http. I have a retry code in my application, and making my own mocks that emulate the whole filesystem or http server miss the point of having a mocking framework.
@andreib1 @brevno , I was in the same situation as us guys but I think @rayzyar pointed at the actual solution.
As you stated, the problem is testing: a. Concurrency b. With Call.Return value that is affected by the func's arguments
I tried the hack mention, but as told, It wasn't concurrent safe so then I tested using https://github.com/vektra/mockery#return-value-provider-functions
It actually worked, implementing the function but in Return
and not in Run
has worked for concurrent calling.
I'm using testify version 1.4.0
If you came across this post like i did, and all you need to do is change the call return after n calls, try the answer from: https://stackoverflow.com/questions/46374174/how-to-mock-for-same-input-and-different-return-values-in-a-for-loop-in-golang
Use the Times(i int)
func with the mock.Call library like so: (I put the Once to be explicit):
mock.On(...).Return(...).Times(**n**)
mock.On(...).Return(...).Once()
Not sure if this addresses above concerns on concurrency, but i assume it would if all you care about is to return a call with x after n times, then return y after. Cheers!
Before I found this issue just now, I had come up with something not unlike one of the solutions above.
var call *mock.Call
call = stub.On("MyFunc", ugly, mock.MatchedBy(func(hacky *dodgy) bool {
// A horrible hack because mock.Call does not support dynamically computed return values.
// (Calling call.Return() would hang attempting to acquire a non-reentrant lock held by MatchedBy().)
call.ReturnArguments = []interface{}{hacky, nil}
return true
}))
This code just shows the principle, the details are elided. Not pretty, but in my particular case much better than the alternative of lots of repetitive code. Concurrency (the possibility that two threads might call MyFunc around the same time) wasn't an issue in my case.
Tthis problem should at least have how-to documentation in testify, i.e., other than in an issue: users expect the facility to be there and are wasting time searching for something that doesn't exist instead of stepping outside testify to solve it, as Ernesto demonstrates.
This is a 4 year old issue and the original maintainers are gone. I'm happy to take a serious look at this but from a quick scan, I think I see differing problem statements and differing solution suggestions.
The main thing I think I see is people asking for dynamic return values based on the original arguments... is that assesement correct?
(I know, it's been 4 years and it's probably frustrating but we're trying to get the backlog smaller and specifically cleaned up :smile: )
Thank you, Martijn. I'm a new user, but your assessment is correct.
From my point of view, although it wasn't what I did, ernesto-jimenez's solution of a wrapper (with a call to Called()) looks like the most appropriate way to handle it without changing the existing API, so I think it would suffice to document that, with a link to it from the documentation for Call.Return().
Alternatively, a variation of Run() (RunReturn()?) could be added to the API, the return value of which becomes the return value of the call. But then you have to have document how, if that's defined, it takes priority over a Return() call, or maybe whichever was called last takes priority, and there may be other issues. A more general Return() might have been better from the beginning, but the complication of adding an alternative mechanism to the API now may not be justified.
Dynamic/computed returns can be achieved without adding a new feature by doing something like this:
type mockClient struct {
mock.Mock
}
// Perfectly normal testify mock method implementation
func (m *mockClient) Write(context.Context, lib.WriteRequest) (*lib.WriteResponse, error) {
args := m.Called(ctx)
return args.Get(0).(*WriteResponse), args.Error(1)
}
// Any function having the same signature as client.Write()
type WriteFn func(context.Context, lib.WriteRequest) (*lib.WriteResponse, error)
// Expect a call to the mock's Write(ctx, req) method where the return value
// is computed by the given function
func (m *mockClient) OnWrite(ctx, req interface{}, impl WriteFn) *mock.Call {
call := m.On("Write", ctx, req)
call.Run(func(CallArgs mock.Arguments) {
callCtx := CallArgs.Get(0).(context.Context)
callReq := CallArgs.Get(1).(lib.WriteRequest)
call.Return(impl(callCtx, callReq))
})
return call
}
and then consume it in the test:
func TestThing(t *testing.T){
t.Run("write", func(t *testing.T){
client := mockClient{}
client.Test(t)
thing := Thing{Client: &client}
// Keeps track of values written to mock client
records := []lib.Record{}
ctx := mock.Anything
req := lib.WriteRequest{Value: "expected value"}
// If the request doesn't match, the mock impl is not called and the test fails
client.OnWrite(ctx, req, func(_ context.Context, req lib.WriteRequest) (*lib.WriteResponse, error){
id := len(records) // ID is computed
records = append(records, lib.Record{ID: id, Value: req.Value})
return &lib.WriteResponse{ID: id}, nil
}).Once()
thing.DoSomethingTestable("expected value")
client.AssertExpectations(t)
assert.Contains(t, records, lib.Record{ID: 0, Value: "expected value"})
})
}
I would like to emphasize that this type of functionality is considered by many to be bare minimum functionality. The mocking capabilities of testify would be seriously improved if this were to be implemented.
Why this is such a big problem to implement this?
I also just encountered a use case for this, and find it very frustrating that this isn't implemented.
For anyone who is willing to try a different mocking package to get this kind of functionality, I recently stumbled upon: https://github.com/derision-test/go-mockgen
Which perfectly fit this use case. As an added benefit, most of the developer facing API is generated in with strong types :).
I see #742 was opened to address this. Seems to be blocked waiting for a review by a maintainer?
Got this working using multiple function returns:
First, declaring my mock:
func (g *testGitHubAPI) FilesContent(ctx context.Context, owner string, repo string, branch string, filepaths []string) (github.FileContentByPath, error) {
args := g.Called(ctx, owner, repo, branch, filepaths)
// Return functions instead of values to allow custom results based on inputs
return args.Get(0).(func(ctx context.Context, owner, repo, branch string, filepaths []string) github.FileContentByPath)(ctx, owner, repo, branch, filepaths),
args.Get(1).(func(ctx context.Context, owner, repo, branch string, filepaths []string) error)(ctx, owner, repo, branch, filepaths)
}
Then by returning multiple returns based on the input to the functions, note the one function per return value:
ghAPI := &testGitHubAPI{}
ghAPI.On("FilesContent", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("[]string")).
Return(
// One function return per return value is required
func(ctx context.Context, owner, repo, branch string, filepaths []string) github.FileContentByPath {
if len(filepaths) > 0 && filepaths[0] == "a.json" {
return github.FileContentByPath{
"a.json": `{"name": "a"}`,
}
} else if len(filepaths) > 0 && filepaths[0] == "b.json" {
return github.FileContentByPath{
"b.json": `{"name": "b"}`,
}
}
return github.FileContentByPath{}
},
func(ctx context.Context, owner string, repo string, branch string, filepaths []string) error {
return nil
})
I've got it by some code in method: Instead of:
func (m *MockObject) Get(data int) int {
args := m.Called(data)
return args.Int(0)
}
Use this:
func (m *MockObject) Get(data int) int {
args := m.Called(data)
type methodSignature = func(int) int
switch args.Get(0).(type) {
case methodSignature:
return args.Get(0).(methodSignature)(data)
default:
return args.Int(0)
}
}
In test you can use it as you want:
mockObject := new(MockObject)
mockObject.On("Get", 1).Return(1)
mockObject.On("Get", mock.Anything).Return(func(data int) int {
return data - 1
})
@mvdkleijn What's the status of this? This would be pretty simple to implement by adding a property to Call but there are already 150 outstanding PRs, many of which are months or years old. So even if the maintainers are ok with this change, I'm not optimistic that it would get merged.