testify icon indicating copy to clipboard operation
testify copied to clipboard

Allow dynamic returns based on arguments

Open alexandrevicenzi opened this issue 8 years ago • 31 comments

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.

alexandrevicenzi avatar Sep 15 '16 17:09 alexandrevicenzi

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 avatar Sep 15 '16 20:09 ernesto-jimenez

@ernesto-jimenez I fixed the example.

I'll try your example.

alexandrevicenzi avatar Sep 15 '16 20:09 alexandrevicenzi

@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 avatar Sep 15 '16 20:09 alexandrevicenzi

@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 avatar Sep 24 '16 13:09 ernesto-jimenez

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

alexandrevicenzi avatar Oct 05 '16 19:10 alexandrevicenzi

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

ernesto-jimenez avatar Oct 05 '16 19:10 ernesto-jimenez

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

rayzyar avatar Nov 14 '16 06:11 rayzyar

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

sivachandran avatar Sep 08 '17 10:09 sivachandran

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.

peterhuene avatar Oct 06 '17 22:10 peterhuene

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

ernesto-alvarado avatar May 09 '19 12:05 ernesto-alvarado

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?

csmadhav avatar Aug 14 '19 08:08 csmadhav

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

brevno avatar Nov 29 '19 14:11 brevno

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 avatar Jan 03 '20 06:01 hieuvo

@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 avatar Jan 08 '20 14:01 andreib1

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

brevno avatar Jan 08 '20 15:01 brevno

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 avatar Jan 08 '20 15:01 andreib1

@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

Gilwe avatar Feb 16 '20 10:02 Gilwe

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!

ianhecker avatar Apr 13 '20 18:04 ianhecker

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.

michael-db avatar Aug 07 '20 07:08 michael-db

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

mvdkleijn avatar Aug 09 '20 08:08 mvdkleijn

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.

michael-db avatar Aug 10 '20 05:08 michael-db

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

IronSavior avatar Oct 05 '20 17:10 IronSavior

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.

kasvtv avatar Jan 24 '22 17:01 kasvtv

Why this is such a big problem to implement this?

ashep avatar Feb 13 '22 10:02 ashep

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

KyleFromKitware avatar Apr 05 '22 19:04 KyleFromKitware

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

kasvtv avatar Apr 06 '22 16:04 kasvtv

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

ninthclowd avatar May 11 '22 19:05 ninthclowd

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

DavidGamba avatar Jul 04 '22 04:07 DavidGamba

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

backdround avatar Nov 23 '22 18:11 backdround

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

firelizzard18 avatar Jul 13 '23 21:07 firelizzard18