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

Allow dynamic returns based on arguments #350

Closed
alexandrevicenzi opened this issue Sep 15, 2016 · 33 comments · May be fixed by #742
Closed

Allow dynamic returns based on arguments #350

alexandrevicenzi opened this issue Sep 15, 2016 · 33 comments · May be fixed by #742

Comments

@alexandrevicenzi
Copy link

alexandrevicenzi commented Sep 15, 2016

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.

@ernesto-jimenez
Copy link
Member

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"))

@alexandrevicenzi
Copy link
Author

@ernesto-jimenez I fixed the example.

I'll try your example.

@alexandrevicenzi
Copy link
Author

@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.

@ernesto-jimenez
Copy link
Member

@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.

@alexandrevicenzi
Copy link
Author

@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").

@ernesto-jimenez
Copy link
Member

ernesto-jimenez commented Oct 5, 2016

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)
}

@rayzyar
Copy link

rayzyar commented Nov 14, 2016

@alexandrevicenzi looks like it's solved here https://github.com/vektra/mockery#return-value-provider-functions

@sivachandran
Copy link

@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.

@peterhuene
Copy link

peterhuene commented Oct 6, 2017

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.

@ernesto-alvarado
Copy link

Almost any other mocking framework (even some golang ones) allows for this, what is the problem of having it also in this one?

@csmadhav
Copy link

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?

@brevno
Copy link

brevno commented Nov 29, 2019

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}
		}
	}
})

@hieuvo
Copy link

hieuvo commented Jan 3, 2020

this was supported by testify already, it's RunFn

RunFn func(Arguments)

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}
		}
	}

@andreib1
Copy link

andreib1 commented Jan 8, 2020

@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?

@brevno
Copy link

brevno commented Jan 8, 2020

@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.

@andreib1
Copy link

andreib1 commented Jan 8, 2020

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.

@Gilwe
Copy link

Gilwe commented Feb 16, 2020

@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

@ianhecker
Copy link

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!

@michael-db
Copy link

michael-db commented Aug 7, 2020

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.

@mvdkleijn
Copy link
Contributor

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 😄 )

@michael-db
Copy link

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.

@IronSavior
Copy link

IronSavior commented Oct 5, 2020

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"})
	})
}

@kasvtv
Copy link

kasvtv commented Jan 24, 2022

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.

@ashep
Copy link

ashep commented Feb 13, 2022

Why this is such a big problem to implement this?

@KyleFromKitware
Copy link

I also just encountered a use case for this, and find it very frustrating that this isn't implemented.

@kasvtv
Copy link

kasvtv commented Apr 6, 2022

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 :).

@ninthclowd
Copy link

I see #742 was opened to address this. Seems to be blocked waiting for a review by a maintainer?

@DavidGamba
Copy link

DavidGamba commented Jul 4, 2022

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
				})

@backdround
Copy link

backdround commented Nov 23, 2022

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
})

@firelizzard18
Copy link

@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.

@pbmlunar
Copy link

Seems like testify is dead code as of right now.

I also ran into this issue and used the hackish solutions described above. Guess the hack is the "official" solution as the officials maintainers are gone.

@kimbuba
Copy link

kimbuba commented Sep 10, 2024

@alexandrevicenzi looks like it's solved here https://github.com/vektra/mockery#return-value-provider-functions

As Alex mentioned it works very well. It's just a code generator that works with tesitfy.

Run it with
mockery --all --with-expecter
and it will generate the mocks for all your interfaces and add the EXPECT() method which will allow you to use the method at compile time and you'll be able to RunAndReturn whatever you want.

mockAwsService.EXPECT().S3PresignedGetRequest(mock.AnythingOfType("string")).RunAndReturn(func(key string) (string, error) { return key, nil })

@brackendawson
Copy link
Collaborator

The solution documented above and implimented in mockery is in my opinion sufficient for those who want to create mixed mocks and fakes.

@brackendawson brackendawson closed this as not planned Won't fix, can't repro, duplicate, stale Sep 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.