-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve vararg handling #2807
Improve vararg handling #2807
Conversation
Fixes: mockito#2796 Add an optional method to `VarargMatcher`, which implementations can choose to override to return the type of object the matcher is matching. This is used by `MatcherApplicationStrategy` to determine if the type of matcher used to match a vararg parameter is of a type compatible with the vararg parameter. Where a vararg compatible matcher is found, the matcher is used to match the _raw_ parameters.
...add the `type()` method to the `ArgumentMatcher` interface, rather than the `VarargMatcher` interface. This would allow the, IMHO _broken_, `VarargMatcher` interface to be removed in a later major release.
@TimvdLippe Personally, I prefer this approach. |
Codecov ReportBase: 86.20% // Head: 84.80% // Decreases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## main #2807 +/- ##
============================================
- Coverage 86.20% 84.80% -1.40%
- Complexity 2833 2866 +33
============================================
Files 320 327 +7
Lines 8587 8744 +157
Branches 1060 1057 -3
============================================
+ Hits 7402 7415 +13
- Misses 904 1052 +148
+ Partials 281 277 -4
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some review notes...
src/main/java/org/mockito/internal/invocation/MatcherApplicationStrategy.java
Show resolved
Hide resolved
OK @TimvdLippe , I think that's all the implementations of This should be just about complete now. Look forward to a review when you have time. |
I haven't done a thorough review yet, as I mostly want to discuss the change to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we are almost there 😄
@TimvdLippe I've updated the If possible, I'd prefer we merge this, and I look to remove the |
@big-andy-coates Yes that's okay to me. It might still be a good idea to include the removal of the interface into Mockito 5.0.0 no? Since it removes an interface, even if it is internal, I think it is cleanest to include in a major release. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Woohoo, great work!
Yes, I'll get this done asap to include in the 5.0.0 release. |
See #2835 for pr removing |
Using the new `type()`, we can differentiate between matching all varargs or only one argument of the varargs. # Benefits: Because this approach leaves `VarargsMatcher` untouched, it does not require additional existing matchers to implement `VarargsMatcher` to fix issues such as #567. Where as the first PR would require `Null` and `NotNull` to be marked `VarargsMatcher`. This PR creates new variants of `isNotNull` and `isNull` to address #567. Having `InstanceOf` override `type()` provides a workable solution to #1593. Having `equals` override `type` addresses #1222. # Downsides The obvious downside is that this changes the public `ArgumentMatcher` interface, though in a backwards compatible way. ## Known limitation The main limitation I'm aware of, is not a new limitation. It is that it is not possible to assert only a single parameter is passed to the vararg parameter, when using a `VarargMatcher`, e.g. `any()`. (ref: #1593). For example: ```java // Given method: int vararg(String... args); // I want to mock this invocation: mock.vararag("one param"); // ...but not these: mock.vararg(); mock.vararg("more than", "one param"); ``` There is no current way to do this. This is because in the following intuitive mocking: ```java given(mock.vararg(any(String.class))).willReturn(1); ``` ... matches zero or more vararg parameters, as the `any()` method is using `VarargMatcher`. It seems to me that `VarargMatcher` is... a little broken! This is maybe something that should be consider a candiate for fixing in the next major version bump. While it is not possible to fix any `VarargMatcher` based matchers in a backwards compatible way, this the approach in this PR it is possible to mock/verify exactly one vararg param using `isA`, rather than `any`: ```java @test public void shouldMatchExactlyOnParam() { mock.varargs("one param"); verify(mock).varargs(isA(String.class)); } @test public void shouldNotMatchMoreParams() { mock.varargs("two", "params"); verify(mock, never()).varargs(isA(String.class)); } @test public void shouldMatchAnyNumberOfParams() { mock.varargs("two", "params"); verify(mock).varargs(isA(String[].class)); } ``` ... because `isA` does not implement `VarargsMatcher`, and so can work as expected once it implements `type()`. Fixes #2796 Fixes #567 Fixes #584 Fixes #1222 Fixes #1498
This PR does end up introducing an incompatible change to I gather that this change might not land until the 5.0.0 major release, in which case that's obviously the time to make incompatible changes :) You may want to note the change in a |
Oh, sorry, scratch all that: |
Ah yeah we don't consider |
Thanks. I have to imagine they're possible to port. They might actually mostly be implementations of That's not to say that we'll actually do it anytime soon. (I'm sure you of all people are shocked... :)) But I should at least more readily remember how much this is our fault next time. |
Haha yeah all good. I am sure you can find some of internal tickets with more explanation on how to tackle that problem. Feel free to message me if you need more information from my side. |
Thanks, I had already come across one :) I will also report that people really like writing |
…table*` methods and to `ArgumentCaptor.forClass`. - The `Immutable*` methods of course already throw `NullPointerException`. And while they're already annotated for nullness, they've been ignored by this checker because there are [technical challenges](https://bugs.openjdk.org/browse/JDK-8225377) with identifying the nullness of type-variable types loaded from `.class` files. - `ArgumentCaptor.forClass` soon [will throw `NullPointerException`](mockito/mockito@fe1cb2d#diff-8d274a9bda2d871524d15bbfcd6272bd893a47e6b1a0b460d82a8845615f26daR31). See mockito/mockito#2807 (comment). To match `ArgumentCaptor.forClass`, we have to start checking test-only code. But for now, we check such code _only_ for calls to `ArgumentCaptor.forClass`. (I suppose that we still don't handle the _varargs_ parameters of the various `Immutable*` methods, since this entire check skips varargs, in part because varargs are confusing in general (b/232103314) and in part because we can't see annotations on a varargs-element type that is loaded from a `.class` file. We could make an exception for these methods if we really wanted, but I doubt it's worth it.) PiperOrigin-RevId: 499237604
…table*` methods and to `ArgumentCaptor.forClass`. - The `Immutable*` methods of course already throw `NullPointerException`. And while they're already annotated for nullness, they've been ignored by this checker because there are [technical challenges](https://bugs.openjdk.org/browse/JDK-8225377) with identifying the nullness of type-variable types loaded from `.class` files. - `ArgumentCaptor.forClass` soon [will throw `NullPointerException`](mockito/mockito@fe1cb2d#diff-8d274a9bda2d871524d15bbfcd6272bd893a47e6b1a0b460d82a8845615f26daR31). See mockito/mockito#2807 (comment). To match `ArgumentCaptor.forClass`, we have to start checking test-only code. But for now, we check such code _only_ for calls to `ArgumentCaptor.forClass`. (I suppose that we still don't handle the _varargs_ parameters of the various `Immutable*` methods, since this entire check skips varargs, in part because varargs are confusing in general (b/232103314) and in part because we can't see annotations on a varargs-element type that is loaded from a `.class` file. We could make an exception for these methods if we really wanted, but I doubt it's worth it.) PiperOrigin-RevId: 499326542
Hey @big-andy-coates @TimvdLippe, I'm trying Mockito 5.2 in Android using Kotlin and I got over a problem with varg. Let's say we have the following interface for getting strings:
When I try to mock it using:
works just fine, as expected; it checks if only one argument was used but if I try if I try to mock any of string I don't know how should I express |
@dacianf For Kotlin related questions, please ask it over at |
Fixes: #2796
Fixes: #567
Fixes: #584
Fixes: #1222
Fixes: #1498
An alternative approach to #2805.
Key difference:
This approach adds the new
type()
method to the mainArgumentMatcher
interface and leaves the, IMHO broken,VarargsMatcher
interface alone.Benefit:
Because this approach leaves
VarargsMatcher
untouched, it does not require additional existing matchers to implementVarargsMatcher
to fix issues such as #567. Where as the first PR would requireNull
andNotNull
to be markedVarargsMatcher
.This PR creates new variants of
isNotNull
andisNull
to address #567.Having
InstanceOf
overridetype()
provides a workable solution to #1593.Having
equals
overridetype
addresses #1222.If we want to go with this approach I'll implement
type()
for all the other existing matchers.Downside
The obvious downside is that this changes the public
ArgumentMatcher
interface, though in a backwards compatible way.The default implementation could be dropped in the next major version bump.
Known limitation
The main limitation I'm aware of, is not a new limitation. It is that it is not possible to assert only a single parameter is passed to the vararg parameter, when using a
VarargMatcher
, e.g.any()
. (ref: #1593). For example:There is no current way to do this. This is because in the following intuitive mocking:
... matches zero or more vararg parameters, as the
any()
method is usingVarargMatcher
. It seems to me thatVarargMatcher
is... a little broken! This is maybe something that should be consider a candiate for fixing in the next major version bump.While it is not possible to fix any
VarargMatcher
based matchers in a backwards compatible way, this the approach in this PR it is possible to mock/verify exactly one vararg param usingisA
, rather thanany
:... because
isA
does not implementVarargsMatcher
, and so can work as expected once it implementstype()
.Checklist
including project members to get a better picture of the change
commit is meaningful and help the people that will explore a change in 2 years
Fixes #<issue number>
in the description if relevantFixes #<issue number>
if relevant