-
Notifications
You must be signed in to change notification settings - Fork 607
Define fallback for mock methods #191
base: main
Are you sure you want to change the base?
Conversation
I like the idea of being able to specify default behavior. I would call it "Default" or "WillByDefault" (that's what gmock calls it in C++), or something like that rather than Fallback. I want to think a little about the way this fits in with everything. For example, a lot of caveats have to be given, which makes the interface complicated. Some of the caveats also don't seem necessary. For example, why can't matchers be used to specify varying default behavior? |
I thought of "Fallback" because it tells what it is: a Fallback :) I think a Fallback should be something very simple that does stupidly return some value. I thought of fallback as something like: "I need to mock this but I do not really care what the consumer of this mock does with it. Just mock it and return some static value". But indeed it would be maybe more clean to consider matchers. Looks reasonable |
Function is now called: WillByDefault You can now use matchers, After(), MinTime, MaxTime. So it behaves like other expectations. Indeed seems less complex and less "special" now. |
remove unnecessary comment
@balshetzer what's your opinion? |
I like it in general. I think this will solve several feature requests. I think we went too far in the other direction. I don't think the default behavior should support min and max times or After. I also want to make sure we understand how it fits into gomock in general, for example: |
I think it is okay to provide the usual methods like min, max, etc... Your questions:
One idea could be (didn't look into the code yet to check how complex the change would be): We could also call the method ByDefault() then both would look ok:
|
Hi. I renamed WillByDefault to ByDefault, so it reads well with Returns() before and after ByDefault. All XTimes() methods are not allowed anymore. After() is also not allowed. Using default call as prerequisite is also not allowed. I think this is the best I can provide without changing too much code. I think this is a quick win for providing defaults. |
Thanks. I'll review as soon as I can. |
@balshetzer something new on this? I need this feature very often, could you just give me a hint when you will have time for a review? So I can decide if I use a fork in my projects. |
Hi. Has this been reviewed, yet? |
@balshetzer |
@ybbus Sorry for the delay. As you mentioned earlier, this is an attempt to add the ability for looser mocks. I'm hoping to avoid adding too many more methods as this adds more complexity. Would your use case be solved by #246's solution? This better matches gmock and wouldn't require more methods. WDYT? |
@ybbus Thoughts ^ |
@poy I think this is quite error prone if you define more than one expectation in your tests and forget to reorder them. Also for the reader of a test it may look quite strange if he/she does not knwo about / overlooks the reverseorder flag. I am not a fan of this approach, but still better than no defaults at all. In my opinion an explicit "ByDefault" methods is more clear. |
@poy @ybbus To me this PR is about ergonomics, not about going out of our way to violate anyone's preconception about what a mock library should or shouldn't do. To illustrate what I mean, let me explain my situation:
That last bullet point reaches the crux of why I consider this PR an issue of ergonomics: having to write these "happy path" behaviors as mock expectations over and over again (for the sake of using this library to assert on calls that we really do care about in the boundary condition testing) in ways that tend to mimic the business logic of the code being tested leads to a massive amount of tedious-to-write and difficult-to-read boilerplate. To work around this limitation in gomock while at the same time benefiting from being able to assert on key state-mutating calls while avoiding lengthy and repetitive boilerplate that mimics application business logic for non-essential or non-mutating calls, I have experimented with writing some difficult-to-understand test fixtures that combine the benefits of fake and mock implementations of the same external services that hide the complexity of populating mock method
See the description of my use case above; I suppose reversing the order of call evaluation could do the trick in the sense that it would enable Something else that could help me write the kind of fixture I described would be to be able to detect what call expectations have already been set on a given mock so that I could add the |
To see why I am skeptical of #246, consider:
|
For me in unit tests the most important behavior of mocks is (as already stated) is the happy path and some standard stupid behavior /return value. That's why I am still using a forked version of gomock, where this default mechanism is available, since this is my most used feature for me. If interfaces are quite easy I even just have some standard mock implementation that looks something like this (and is also all about default behavior):
Mock:
This works in 90% of all test cases. And if I need something special, I just replace the method for the specific case (and reset it with a defer). I think a Default() is essential and should be important enough to get its own Method, not by doing some hacky ordering stuff, that a ready hardly understands. |
@ybbus while I think you and I come down on the same side of "let's enable defaults for mock methods" I think it's useful if we all follow the same definition of key terms like "mock". If we take the googlemock definition as our starting point, then the example you gave is a kind of fake implementation rather than a mock (i guess some pedants might call it a stub implementation). But again I essentially agree that as a matter of ergonomics (and ignoring any dogmatic definitions of what a "mock" is or should be) we should be able to provide fake-like behaviors via default method implementations. |
@waynr You are totally right here regarding the definition. I just wanted to emphasize that this Default feature would really add a lot of value to the library and is an essential tool in many cases. |
I'd like to see the requirements and user story clarified here; I think that will help us clearly evaluate a solution. As others have pointed out this comes down to the discussion of mocks vs fakes as conceptualized by Google. I have definitely experienced the UX pain that is being described here; especially with complex interfaces like RPC clients! I am not opposed to adding support for fakes, but I am opposed to expanding the API surface of gomock without a strong value proposition. Breaking changes are basically out of the question (not suggesting what is being proposed would result in a breaking change). As a starting point of discussion, why is creating a "default" expectation by making it the last declared expectation not sufficient?
Because gomock evaluates expectations in the order they were declared, this allows the default to be triggered when no other calls match or all potentially matching calls have been exhausted. If the concern is boilerplate, that seems easily solved with some project specific code
|
@ybbus Would you mind making an issue for this for further discussion? |
@cvgw Convey("Test some functionality", t, func() {
// this will be called before every inner test
mock := NewMock(controller)
mock.EXPECT().MyMethod(gomock.Any()).ByDefault().Return(123, nil)
Convey("Test call fails", func() {
// here we may easily "override" the default
// --> this would not work with your example, since the default would catch everything
mock.EXPECT().MyMethod(gomock.Any()).Return(0, errors.New("some error")
})
Convey("Test standard behavior", func() {
// here we don't need specific behavior and fallback to the default
})
// ... many other tests with or without custom behavior
// called after every inner test
Reset(func() {
controller.Finish()
})
}) |
@codyoss / @poy Can we merge this? This is a pretty small change that will add a lot of value. I was just about to open an issue + PR about the exact same thing and I saw several issues opened regarding this (EX: #238, #246, #137). |
Hey, 5 years later... :) @codyoss I still see big value here, since I use it in a fork of this project all the time. But if you don't, we may close the PR. |
Hi.
I would appreciate your feedback for this pull request.
It is kind of similar to: #188
For each method you can define a fallback / default behaviour by calling:
mockObject.EXPECT().FooMethod().Fallback().Returns(5)
After defining this expectation as fallback, if there is no other expectation that matches a call to this method, the above defined fallback will be executed and return 5 all the time.
This is very helpful to define a default behaviour for methods. This makes more sense than loose mocks since most of the time the default return value is not very handy (e.g. nil, "" etc..).
The following rules for Fallback() should be noted (see the :gomock/controller_test.go for examples)
I tried to change as little as possible in the code for this feature to work.