Skip to content
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

Fixes #3005 #3123

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

Conversation

nineninesevenfour
Copy link

@nineninesevenfour nineninesevenfour commented Sep 25, 2023

Checklist

  • Read the contributing guide
  • PR should be motivated, i.e. what does it fix, why, and if relevant how
  • If possible / relevant include an example in the description, that could help all readers
    including project members to get a better picture of the change
  • Avoid other runtime dependencies
  • Meaningful commit history ; intention is important please rebase your commit history so that each
    commit is meaningful and help the people that will explore a change in 2 years
  • The pull request follows coding style
  • Mention Fixes #<issue number> in the description if relevant
  • At least one commit should mention Fixes #<issue number> if relevant

Fixes several issues with @Injectmocks when generics are involved. Rewrite of matching logic based on resolving the concrete types down the type hierarchy.

@TimvdLippe
Copy link
Contributor

My feedback from #3022 (comment) still stands. This PR is massive and the additional code is not worth the complications that @InjectMocks introduced. Instead, let's work towards moving it into a separate artifact and fix the issue there. We publish this new artifact (we have to use a different name for the annotation) and then mark the one we ship in mockito-core as deprecated. Then, in the next major version we remove the one from mockito-core and anybody else can use the separate artifact. WDYT?

@nineninesevenfour
Copy link
Author

Hmm... not sure, where the separate artefact should exactly hook into.
As far as I can see, only one instance of AnnotationEngine is supported. That is the reason why SpyAnnotationEngine (for @Spy) and IndependentAnnotationEngine(for the other annotations) are created on behalf of InjectingAnnotationEngine. That means InjectingAnnotationEngine cannot just be moved out into a separate component, when you want to keep other annotations in core. So I do not see an easy way to untangle that right now. Additionally the method AutoCloseable process(Class<?> clazz, Object testInstance);is too simple to support @InjectMocks as it is now. More surrounding information is needed (which mocks exist, where do they come frome, etc.).

The situation for the other extensions loaded through PluginLoader seems similar: Please blame me, if I am wrong, but as far as I can see it is always "one or the other" meaning the interfaces do not allow differennt implementations to stack up or exist side by side.

Maybe you have a concrete idea how to achieve what you proposed, then please let me know.

So I propose of the following:

  • merge the PR for now to make the issues go away and hold up Mockitos reputation
  • we start designing a new SPI mechanism for Mockito 6 that allows augmenting and intercepting the mocking process.
  • then with mockito 6 we move out @InjectMocks entirely and put it into a separate module.

Signed-off-by: Harald Fassler <harald.fassler+9974@gmail.com>
@nineninesevenfour
Copy link
Author

@TimvdLippe
Hello Tim,

I have now moved the changes into a separate submodule "inject-generics". To achieve that I made the following steps:

  • Making the interface MockCandidateFilter pluggable through the existing plugin mechanism. However, I did not move it into the org.mockito.plugins package.
  • Introducing a new category of plugins: "internal plugins". The reason is that I think MockCandidateFilter is not suitable as a public plugin API. See: InternalPlugins.java
  • Move the new classes into the submodule together with a new TypeWithGenericsCandidateFilter.
  • Reverted other changes in core related to changing TypeBasedCandidateFilter.

Is that an approach you can live with?

The name of the new submodule is a matter of taste, if you think of a better name, pls. let me know.
Do you know, if the submodule will be automatically published as "mockito-inject-generics"?

Deprecation of @InjectMocks I do not see as a subject of this PR, this should be a separate issue, IMHO. (only one step at a time).

Please let me know, what you think.

@TimvdLippe
Copy link
Contributor

Hey, this approach looks a lot better indeed! Unfortunately I am quite busy atm with work, but will get back to you the weekend after next for a full review.

@codecov-commenter
Copy link

codecov-commenter commented Sep 27, 2023

Codecov Report

Attention: 59 lines in your changes are missing coverage. Please review.

Comparison is base (5d946b4) 85.48% compared to head (47ea146) 85.32%.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #3123      +/-   ##
============================================
- Coverage     85.48%   85.32%   -0.17%     
- Complexity     2892     2896       +4     
============================================
  Files           329      339      +10     
  Lines          8826     9131     +305     
  Branches       1095     1160      +65     
============================================
+ Hits           7545     7791     +246     
- Misses          994     1014      +20     
- Partials        287      326      +39     
Files Coverage Δ
...guration/injection/PropertyAndSetterInjection.java 97.72% <100.00%> (+0.05%) ⬆️
...ion/injection/filter/TypeBasedCandidateFilter.java 96.38% <100.00%> (ø)
...l/configuration/plugins/DefaultMockitoPlugins.java 92.85% <100.00%> (+0.35%) ⬆️
...nternal/configuration/plugins/InternalPlugins.java 100.00% <100.00%> (ø)
...internal/configuration/plugins/PluginRegistry.java 100.00% <100.00%> (ø)
...ockito/internal/configuration/plugins/Plugins.java 100.00% <100.00%> (ø)
...to/internal/util/reflection/generic/MatchType.java 100.00% <100.00%> (ø)
...ernal/util/reflection/generic/MatchArrayClass.java 87.50% <87.50%> (ø)
...o/internal/util/reflection/generic/MatchClass.java 85.71% <85.71%> (ø)
...rnal/util/reflection/generic/VariableResolver.java 91.52% <91.52%> (ø)
... and 5 more

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Signed-off-by: Harald Fassler <harald.fassler+9974@gmail.com>
Signed-off-by: Harald Fassler <harald.fassler+9974@gmail.com>
@TimvdLippe
Copy link
Contributor

As you probably noticed, I haven't had the time to look at this PR. My current precious free time is spent on #3037 which requires our attention at the moment. Hopefully when that discussion settles, we can go back to this PR.

@karniemi
Copy link

karniemi commented Nov 9, 2023

@nineninesevenfour thanks for working on this! My team is also eagerly waiting for this set of fixes. There seems to be some conflicts already, I wonder if you have time to resolve them before @TimvdLippe hops in to review the changes?

Copy link
Contributor

@TimvdLippe TimvdLippe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of code here which took me a while to get through. Overall, I like the regression tests and I understand why all of the implementation is required. Therefore, I think this is good to be optional for users to include, as the overhead of these filters can be large, but sometimes is required to get things right.

I left some comments on small improvements that we need to make, but overall this approach LGTM! Thanks again for the work and apologies for the long review time because of the large content of this PR.

@@ -63,6 +65,8 @@ public class DefaultMockitoPlugins implements MockitoPlugins {
DEFAULT_PLUGINS.put(
DoNotMockEnforcer.class.getName(),
"org.mockito.internal.configuration.DefaultDoNotMockEnforcer");
DEFAULT_PLUGINS.put(
MockCandidateFilter.class.getName(), TypeBasedCandidateFilter.class.getName());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While type-safety is nice, we don't want to refer to the concrete implementations here so to avoid loading the classes if they are overridden. Therefore, let's use the string names here instead like the other plugin definitions.

apply from: "$rootDir/gradle/java-library.gradle"
apply from: "$rootDir/gradle/root/coverage.gradle"

description = "Filter @InjectMocks according to Java's assignment rules for generics"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make this artifact name inject-mocks instead? The longterm plan would be to move @InjectMocks into this package so that users can rely on that instead. While the current artifact would solve the problem with generics, I ultimately want to move @InjectMocks here and move all related machinery into one place.

"integerList ,stringList ,false",
"stringList ,integerList ,false",

"stringList ,wildcardList ,true",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: add a case for "integerList ,wildcardListInteger ,true",

@CsvSource({
"integerList ,integerCollectionBox ,CollectionBox ,collection ,true",
"integerList ,stringCollectionBox ,CollectionBox ,collection ,false",
"integerList ,wildcardCollectionBox ,CollectionBox ,collection ,false",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the previous test stringList was assignable to wildcardList. I would have expected the same here. Why is it false in this case?

* Mockito won't fail to inject (even if mock field name doesn't match under test's field name).
*/
@ExtendWith(MockitoExtension.class)
public class GenericTypeMockTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love these!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants