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

MockitoMockMaker provides mocking of static methods #1756

Merged
merged 10 commits into from
Feb 22, 2024

Conversation

AndreasTu
Copy link
Member

@AndreasTu AndreasTu commented Aug 13, 2023

MockitoMockMaker supports to mock static methods
of Java classes with the new Spec API:

SpyStatic(Class)
SpyStatic(Class, IMockMakerSettings)

This also supports Java callers of the static methods.

Usage sample:

given:
SpyStatic(StaticClass)
when:
def result = StaticClass.staticMethod()
then:
1 * StaticClass.staticMethod() >> MOCK_VALUE
result == MOCK_VALUE

@codecov
Copy link

codecov bot commented Aug 13, 2023

Codecov Report

Attention: Patch coverage is 94.41341% with 10 lines in your changes are missing coverage. Please review.

Project coverage is 81.53%. Comparing base (2c7db77) to head (9ece5c8).
Report is 15 commits behind head on master.

Files Patch % Lines
...ork/mock/runtime/mockito/MockitoMockMakerImpl.java 83.67% 6 Missing and 2 partials ⚠️
...rg/spockframework/mock/runtime/MockController.java 93.33% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1756      +/-   ##
============================================
+ Coverage     80.44%   81.53%   +1.08%     
- Complexity     4337     4470     +133     
============================================
  Files           441      442       +1     
  Lines         13534    13840     +306     
  Branches       1707     1735      +28     
============================================
+ Hits          10888    11284     +396     
+ Misses         2008     1912      -96     
- Partials        638      644       +6     

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

@AndreasTu AndreasTu force-pushed the MockitoStatic branch 2 times, most recently from 9f31d01 to c3a79b7 Compare August 13, 2023 21:08
@AndreasTu AndreasTu self-assigned this Aug 14, 2023
@AndreasTu AndreasTu force-pushed the MockitoStatic branch 3 times, most recently from 923ee45 to e1161db Compare October 2, 2023 13:29
@AndreasTu AndreasTu marked this pull request as ready for review October 2, 2023 13:31
@AndreasTu
Copy link
Member Author

@leonard84 As a hint, I needed to push the ThreadLocal data into the MockitoInlineMockMakerImpl, because the thread-local behavior of Mockito only works, if you create a new MockMaker.StaticMockControl for each thread.
So the MockitoInlineMockMakerImpl needs to create a StaticMockControl for every Thread, which will activate the static mocks, so we can't handle the ThreadLocal-State in the IStaticMockController.

I also adapted the javadoc on IMockMaker.IStaticMock that different 3rd-party implementations need to handle the thread-local stuff on their own.

@AndreasTu
Copy link
Member Author

AndreasTu commented Oct 4, 2023

I have created an issue in the Mockito repo to request the API for the same inline mockMaker instance, and not relying on private method from Mockito in the method org.spockframework.mock.runtime.mockito.MockitoInlineMockMakerImpl.resolveInlineMockMaker()

@AndreasTu AndreasTu force-pushed the MockitoStatic branch 5 times, most recently from 155b00a to fa28c2e Compare October 11, 2023 14:21
leonard84
leonard84 previously approved these changes Oct 12, 2023
Copy link
Member

@leonard84 leonard84 left a comment

Choose a reason for hiding this comment

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

LGTM, but I'd like some additional feedback from @spockframework/supporter as we are now extending the API in Specification in a bigger way.

docs/extensions.adoc Outdated Show resolved Hide resolved
docs/interaction_based_testing.adoc Outdated Show resolved Hide resolved
docs/interaction_based_testing.adoc Outdated Show resolved Hide resolved
docs/interaction_based_testing.adoc Outdated Show resolved Hide resolved
@AndreasTu AndreasTu changed the title MockitoInlineMockMaker provides mocking of static methods MockitoMockMaker provides mocking of static methods Oct 12, 2023
@AndreasTu AndreasTu force-pushed the MockitoStatic branch 2 times, most recently from 03c27d4 to 66db1f5 Compare October 13, 2023 06:56
@AndreasTu AndreasTu force-pushed the MockitoStatic branch 2 times, most recently from 93ded6f to 4257692 Compare October 25, 2023 17:59
@leonard84
Copy link
Member

A thing that came to mind is, whether we should use global: true or static: true for java mocks instead of adding these new methods.

given:
Mock(StaticClass, static: true)
when:
def result = StaticClass.staticMethod()
then:
1 * StaticClass.staticMethod() >> MOCK_VALUE
result == MOCK_VALUE

@AndreasTu
Copy link
Member Author

@leonard84 that would not work as you might expect. The Mock/Stub/SpyStatic do two things differently as the others methods.

  1. The return void because there is not mock.
  2. the interaction do not delegate to the type <T> instance, but to the Class<T>.
    If we use the existing Mock/Stub/Spy methods it would also break the interactions closures.

@leonard84
Copy link
Member

leonard84 commented Feb 20, 2024

The return void because there is not mock.

We could just return null and document that fact.

the interaction do not delegate to the type instance, but to the Class.
If we use the existing Mock/Stub/Spy methods it would also break the interactions closures.

Correct me if I'm wrong, but the declarations look indentical, except that you leave out the @ClosureParams(FirstParam.FirstGenericType.class), so what would break here?

  @Beta
  public <T> T Mock(
    @DelegatesTo.Target
      Class<T> type,
    @DelegatesTo(strategy = Closure.DELEGATE_FIRST, genericTypeIndex = 0)
    @ClosureParams(FirstParam.FirstGenericType.class)
      Closure interactions) {
    throw invalidMockCreation();
  }

  public <T> void MockStatic(
    @DelegatesTo.Target
    Class<T> type,
    @DelegatesTo(strategy = Closure.DELEGATE_FIRST, genericTypeIndex = 0)
    Closure<?> interactions) {
    throw invalidMockCreation();
  }

@leonard84
Copy link
Member

I was wondering IIRC the inline mocking of Mockito would actually enable global mocking, as it instruments every class looks up if the current instance is "mocked" so if you return always true, then it would behave like a global mock right?

@AndreasTu
Copy link
Member Author

except that you leave out the @ClosureParams(FirstParam.FirstGenericType.class), so what would break here?

So the @ClosureParams would suggest that the parameter passed to the interaction closure is an instance of Class<T>, but there is no instance we can pass to that Closure in the static case, because we do not create one, or maybe we even can't create one.
And the delegate is working because we can delegate to the class instance instead.

@AndreasTu
Copy link
Member Author

I was wondering IIRC the inline mocking of Mockito would actually enable global mocking, as it instruments every class looks up if the current instance is "mocked" so if you return always true, then it would behave like a global mock right?

Theoretically yes, but this would at least require more API on the org.mockito.plugins.MockMaker interface, that we can register a global callback for every instance of a certain class. Currently the API only provides such callbacks for an object created by mockito not for any object. See org.mockito.plugins.MockMaker.createMock() and org.mockito.plugins.MockMaker.createSpy()

We could ask the mockito guys, how they would feel about that.

But what do you want to accomplish with that?

@leonard84
Copy link
Member

leonard84 commented Feb 20, 2024

except that you leave out the @ClosureParams(FirstParam.FirstGenericType.class), so what would break here?

So the @ClosureParams would suggest that the parameter passed to the interaction closure is an instance of Class<T>, but there is no instance we can pass to that Closure in the static case, because we do not create one, or maybe we even can't create one.
And the delegate is working because we can delegate to the class instance instead.

You are mistaken, if you leave out the explicit param it will use it if you look at the generated code via gw coreConsole or an AST test, you can see that

    MockStatic(StaticClass) {
      staticMethod() >> "MockValue"
    }

will be compiled to

    this.MockStaticImpl(null, null, StaticClass, { ->
        this.getSpecificationContext().getMockController().addInteraction(new org.spockframework.mock.runtime.InteractionBuilder(3, 7, 'staticMethod() >> \"MockValue\"').addEqualTarget(it).addEqualMethodName('staticMethod').setArgListKind(true, false).addConstantResponse('MockValue').build())
    })

it doesn't use the delegate but .addEqualTarget(it).

The closure parameter is only used to rename it to something else

    MockStatic(StaticClass) { sc ->
      sc.staticMethod() >> "MockValue"
    }
    this.MockStaticImpl(null, null, StaticClass, { java.lang.Object sc ->
        this.getSpecificationContext().getMockController().addInteraction(new org.spockframework.mock.runtime.InteractionBuilder(3, 7, 'sc.staticMethod() >> \"MockValue\"').addEqualTarget(sc).addEqualMethodName('staticMethod').setArgListKind(true, false).addConstantResponse('MockValue').build())
    })

and here .addEqualTarget(sc)

@leonard84
Copy link
Member

I was wondering IIRC the inline mocking of Mockito would actually enable global mocking, as it instruments every class looks up if the current instance is "mocked" so if you return always true, then it would behave like a global mock right?

Theoretically yes, but this would at least require more API on the org.mockito.plugins.MockMaker interface, that we can register a global callback for every instance of a certain class. Currently the API only provides such callbacks for an object created by mockito not for any object. See org.mockito.plugins.MockMaker.createMock() and org.mockito.plugins.MockMaker.createSpy()

We could ask the mockito guys, how they would feel about that.

But what do you want to accomplish with that?

Well, the same as in Groovy, it intercept things that you could't create and inject yourself.
For example, a singleton that uses instance methods.

@AndreasTu
Copy link
Member Author

AndreasTu commented Feb 20, 2024

Yes it is the Class<T> instance and not T during runtime inside of the closure.
It is more for the IDE not to try to complete and display the type for T.
My point is, that the User should not be mislead by the type inference displaying the type T.

You can see the runtime resolution in the class org.spockframework.lang.SpecInternals.createStaticMock():
Where it is called with GroovyRuntimeUtil.invokeClosure(closure, type); which sets the param to type.

Maybe in general: I find the behaviour and user expectation of static mocks different enough to justify the new *Static() methods.
But if the new API in MockingApi is the main concern, I also can live with the mock option static=true.
But this would get me back to the discussion in #1878 about the mock options.

@leonard84
Copy link
Member

Maybe in general: I find the behaviour and user expectation of static mocks different enough to justify the new *Static() methods.
But if the new API in MockingApi is the main concern, I also can live with the mock option static=true.

I think they are comparable to global: true and think we should go with static: true.

@leonard84 leonard84 added this to the 2.4 milestone Feb 22, 2024
@AndreasTu
Copy link
Member Author

I will change the API to onyl provide:

*SpyStatic(Class)
*SpyStatic(Class, IMockMakerSettings)

instead of the static:true and the other methods, because the main usage is to intercept a single static method with SpyStatic instead of mocking all static methods of a class.

@AndreasTu AndreasTu merged commit 9a0bbbc into spockframework:master Feb 22, 2024
23 checks passed
@AndreasTu AndreasTu deleted the MockitoStatic branch February 22, 2024 20:00
Copy link
Member

@Vampire Vampire left a comment

Choose a reason for hiding this comment

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

Sorry that I didn't have time to have a look at this earlier.
I had a cursory look now (mainly at the docs) and generally quite like it.
Some points though:

  • It seems there used to be StubStatic and MockStatic too. I actually wonder why they were removed and there is also no "A la carte" way to create the mock for example with a different or custom defaultResponse. Is that intentional, or can we add this? I mean, you can of course easily use StaticClass._ >> { ZeroOrNullResponse.INSTANCE.respond(delegate) } and StaticClass._ >> _ to achieve the same, it is just not as convenient. :-)
  • "Used to be there" I assumed, because it is still in the docs in the mockito mock maker section: "Mock/Stub/SpyStatic", and also the plural form in "The semantics are the same as for the non-static variants:", and the plural form in "The static mock methods will".
  • The whole documentation of the feature is extremely unlucky, because the "Mocking Static Methods" chapter is a sub-chapter of "Groovy Mocks" as you just extended the section about global Groovy mocks. This has to be split or at least moved. I'm not sure whereto, but probably to "Other Kinds of Mock Objects". If you look at the all-in-one page, you can quite nicely see the overall structure on the left.

@Vampire
Copy link
Member

Vampire commented Mar 2, 2024

And the link in the release notes on the all-in-one page is broken

@AndreasTu
Copy link
Member Author

@Vampire I will open up a new PR to fix the documentation issues.
To your questions:

It seems there used to be StubStatic and MockStatic too. I actually wonder why they were removed

Yes there were also MockStatic/StubStatic, but after a long discussion with @leonard84 we decided to remove them together with all the option and interaction overloads to not complicate the MockingApi with so many methods.
Especially a user should probably only mock 1 or 2 static methods of a Class in a certain test case.
To take over all the static methods of a class is far more invasive than for a single mock instance.
So we thought, just start with the SpyStatic variant and wait, if this would be sufficient.

And yes you ca still do that as you decribed with the interaction, so there is also a work around, I will document that.

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

5 participants