-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
Description
When calling Unset, any expected call that satisfies the arguments in the call being unset is removed from ExpectedCalls.
For example, in the following scenarios, both the Anything, Anything and 3,3 calls are removed:
// Scenario 1- Explicit Unset on the call
mockedService.
On("TheExampleMethod1", Anything, Anything).Return(0).
On("TheExampleMethod1", 2, 2).Return(0)
callToUnset := mockedService.On("TheExampleMethod1", 3, 3).Return(0)
callToUnset.Unset()
// Scenario 2 - Adding/Unsetting a call
mockedService.
On("TheExampleMethod1", 2, 2).Return(0).
On("TheExampleMethod1", 3, 3).Return(0).
On("TheExampleMethod1", Anything, Anything).Return(0)
mockedService.On("TheExampleMethod1", 3, 3).Unset()Reproduction
Go Playground is timing out on build when github.com/stretchr/testify/mock is used (likely due to dependencies downloading), so submitted #1622 with tests to reproduce.
Step To Reproduce
Run any/all of the following tests:
- Test_Mock_UnsetOnlyUnsetsCurrentCall
- Test_Mock_UnsetOnlyUnsetsExactArgsMatchCall
- Test_Mock_UnsetOnlyUnsetsExactArgsMatchCallWhenPartialMatch
- Test_Mock_UnsetOnlyUnsetsExactArgsMatchCallWhenAnythingSomeArg
- Test_Mock_UnsetOnlyUnsetsExactArgsMatchCallWhenAnythingEveryArg
Expected behavior
Only the call(s) with the exact matched arguments should be unset/removed.
Ideally only the call that Unset is invoked against is removed but the current API is written in a manner that requires a Call to unset so unless the call is tracked (e.g., c := mockService.On(....)), On must be called first then Unset which adds the expectation only to immediately remove it along with any other "matched" expectations. #982 which added Unset actually started with Unset on Mock but then moved it to Call to support Unset on a Call but possibly it could have considered leaving Unset(methodName string, arguments ...interface{}) on Mock to remove all "matched expectations" and then Unset on Call to remove just that call expectation.
At minimum, the docs should be updated to reflect actual behavior (see first item in additional information below).
Actual behavior
All calls that satisfy the arguments are removed
Current Workarounds
- Clear
ExpectedCalls- This guidance has been given in several issues (including question / feature-suggestion: built-in way to overwrite / clear ExpectedCalls? #558 which spawnedUnsetbeing introduced) but has the downside of clearing all expected calls, not just the desired call - Walk Expected Calls and manually look for the match - While technically possible, there are private methods in
testifythat have all this logic already (a newfindExactMatchis likely needed) and re-creating this is not trivial. - Walk Expected Calls and remove all calls for a method - This is rather straightforward to do but has two downsides: 1) It requires all expectations for that method to be setup again; 2) It's not chainable
Additional Information
- Why this should be considered a bug vs expected behavior - The docs indicate "removes a mock handler from being called." (emphasis mine). However, it will remove 1 to N handlers where N is any handler that "satisfies" the arguments in the call since it removes all of them (vs. "a" handler). As mentioned above, the API requires calling
Onfirst (unless the call is tracked) which adds a new call then unsets it (and any others that satisfy) vs. just having aUnsetonMockthat will remove any that match, leading to the 1 to N vs. 0 to 1 behavior. Beyond just not matching the docs, regarding why this is relevant, question / feature-suggestion: built-in way to overwrite / clear ExpectedCalls? #558 covers this well but in short, in a complicated Mock, it is common to setup default expectations and then within an individual test, change those expectations for just that test. For example, if an interface has 10 methods all of which a set of tests use but each test is testing individual portions, aNewMyServiceMockutility method (aka. builder) could establish defaults which the test than can override/reset/clear. In this case, each test gets a new Mock but the mock is created byNewMyServiceMockwith defaults. Depending on what the defaults are, there is currently no way to change expectations of specific arguments (vs. any expectation that satisfies the arguments). - The one benefit of the current code is that it allows for "clearing all" expectations for a given method (e.g.
On("TheExampleMethod1", Anything, Anything).Unset()would remove all expectations fromTheExampleMethod1that had two parameters) as there isn't an otherwise available API for this currently. This is likely an unintended benefit however, and possibly creating anUnsetAll(methodName string)would continue to support this for anyone taking advantage of it. - Reviewing the code, I believe this is occurring because
Unsetrelies on logic similar to MethodCalled and therefore detects any remaining available expectation that satisfies the arguments vs. the exact arguments. - I'm happy to work on a PR for this however prior to doing so, want to solicit feedback on whether or not there is agreement on the issue described being a bug (vs. expected behavior) and assuming yes, it is a bug, confirming the expected behavior. My proposed solution would be as follows:
- Unset should do an exact match on args
- All exact matches would be removed from expectations
- if no matches found, it's a no-op - Technically, due to the way unset currently works, the call already has to exist at least once since
On("TheExampleMethod1", Anything, Anything).Unset()actually adds the expectation and then removes it (along with any others that match).
Alternative Solution
If its determined that this is not a bug or even if it is a bug but its determined that changing behavior would not be desirable, alternate approaches could be:
Option 1
- Add
UnsetOnly(or similar) toCallto remove only the call its invoked against - Add
UnsetAll()toMockto remove all expectations - Add
UnsetMethod(methodName string)toMockto remove all expectations for a method - Add
UnsetCall(string methodName, methodName string, arguments ...interface{})toMockwhich would behave identical to whatCall.Unset()does currently but avoids having to callOnonly to then callUnset - Add
UnsetExactCall(string methodName, methodName string, arguments ...interface{})toMockto remove all calls that have exact matched arguments
** Could combine 3 & 4 toUnsetCall(exact bool, string methodName, methodName string, arguments ...interface{})
Option 2
- Expose, either directly or indirectly, findClosestCall, findExpectedCall and
findExactCalls(which would be new and return 0 to N calls) to provide package consumers control over manually manipulatingMock.ExpectedCalls. This is likely less than ideal but at least would give consumers of package an option to accomplish the desired behavior
Related Issues
#1511 proposes a Replace API which is often why Unset is needed. If #1511 were merged currently, it would replace all "matched" expectations which wouldn't necessarily be only one expectation