stretchr / testify

A toolkit with common assertions and mocks that plays nicely with the standard library
MIT License
23.19k stars 1.59k forks source link

Allow dynamic returns based on arguments #350

Closed alexandrevicenzi closed 6 days ago

alexandrevicenzi commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

@ernesto-jimenez I fixed the example.

I'll try your example.

alexandrevicenzi commented 8 years ago

@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 commented 8 years ago

@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 commented 8 years ago

@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 commented 8 years ago

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 commented 7 years ago

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

sivachandran commented 7 years ago

@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 commented 7 years ago

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 commented 5 years ago

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

csmadhav commented 5 years ago

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 commented 4 years ago

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 commented 4 years ago

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}
        }
    }
andreib1 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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

michael-db commented 4 years ago

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 commented 4 years ago

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 commented 2 years ago

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 commented 2 years ago

Why this is such a big problem to implement this?

KyleFromKitware commented 2 years ago

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

kasvtv commented 2 years ago

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 commented 2 years ago

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

DavidGamba commented 2 years ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 3 months ago

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 commented 3 weeks ago

@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 commented 6 days ago

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