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

About this issue

  • Original URL
  • State: open
  • Created 8 years ago
  • Reactions: 53
  • Comments: 30 (3 by maintainers)

Most upvoted comments

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

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.

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

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

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

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

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

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!

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

Why this is such a big problem to implement this?

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

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