Skip to content

Conversation

@m-koops
Copy link
Contributor

@m-koops m-koops commented Dec 6, 2025

This PR is another step of improving suspend function support in Mockito-Kotlin as discussed in #553.

This PR specifically focusses on documenting the stubbing API in Mockito-Kotlin, as well as unifying the various stubbing methods towords a unified model:

  • to start a stubbing operation with whenever(mock.methodCall()) / whenever(mock).methodCall() / whenever { mock.methodCall() } or on(mock.methodCall()) / on { mock.methodCall() }; these methods supply universal support for synchronous and suspendable methods/functios, as well as support for methods/function stubs with generics return types.
  • specific stubbing functions for suspendable functions, e.g. wheneverBlocking { mock.methodCall() } and onBlocking { mock.methodCall() } have been declared deprecated to unify towards the abovementioned stubbing functions whenever { mock.methodCall() } and on { mock.methodCall() }.
  • Likewise the stubbing function onGeneric { mock.methodCall() } have been deprecated in favor of on { mock.methodCall() }.
  • Some stubbing functions have been enhanced to infix functions and/or inline function with reified generic type to provide easier/cleaner to use stubbing API to the users of this library.

The unit tests now tend more towards applying a notation with lambdas for specifying methodCalls and answer specification, using infix notation where posible, e.g.:

        val mock = mock<SynchronousFunctions> {
           on { stringResult() } doAnswer { "result" }
       }

This signifies a move away from mere aliasing/casting functions on top of classic Mockito syntax, towards a more idiomtic Kotlin syntax to stub mocks. More prominent application of lamdas allows us to let the stubbing API evolve in the direction of a clean mocking DSL. This universal idiomtic Kotlin syntax is fully leveraging the powers of lambdas, infix functions, and reified generics in inline functions.

But of course, all alternative notations still apply and are covered with unit tests as well to demonstrate these alternative notations.

@m-koops m-koops force-pushed the document-stubbing-api branch 2 times, most recently from 9a2d974 to 3011f9f Compare December 7, 2025 20:59
@TimvdLippe
Copy link
Contributor

@jselbo Do you mind reviewing this one as well?

@m-koops m-koops marked this pull request as draft December 15, 2025 21:11
… apply whenever() with lambda arg as default in test cases). Also, deprecated methods onBlocking(methodCall), onGeneric(methodCall), wheneverBlocking(methodCall) to unify towards whenever(methodCall) and on(methodCall) as universal methods to stub any method call, being either synchronous, suspendable or with generics return type.
@m-koops m-koops force-pushed the document-stubbing-api branch from 3011f9f to d3240af Compare December 16, 2025 19:34
@m-koops
Copy link
Contributor Author

m-koops commented Dec 16, 2025

This PR has been rebased, updated and completed after recent merges. I consider it ready for review.

@m-koops m-koops marked this pull request as ready for review December 16, 2025 19:36
@jselbo
Copy link
Contributor

jselbo commented Dec 16, 2025

It's a really big PR. Let me take a closer look tomorrow morning.

@jselbo
Copy link
Contributor

jselbo commented Dec 17, 2025

Overall I think this is a good change and moves toward simplifying the API surface. I have some comments:

Some stubbing functions have been enhanced to infix functions and/or inline function with reified generic type to provide easier/cleaner to use stubbing API to the users of this library.

Just taking an example, how does this method benefit from having reified type if it does not use T::class.java in its body? inline fun <reified T> whenever(methodCall: T): OngoingStubbing<T>

It's not a huge deal, but it breaks a handful of our tests that used generics for method stubbing. Example:

fun <T> stubHelper(returnValue: T) {
    whenever(mock.myGenericMethod<T>()).thenReturn(returnValue)
}

Kotlin now says: Cannot use 'T' as reified type parameter. Use a class instead.

The fix is easy at least, to make the helper inline+reified as well. But what's the reason for the change? All the tests still pass when removing the reified from the signature.

This signifies a move away from mere aliasing/casting functions on top of classic Mockito syntax, towards a more idiomtic Kotlin syntax to stub mocks. More prominent application of lamdas allows us to let the stubbing API evolve in the direction of a clean mocking DSL. This universal idiomtic Kotlin syntax is fully leveraging the powers of lambdas, infix functions, and reified generics in inline functions.

I am fine with the alternative DSL-syntax, and it might be preferable for some (also matches MockK style), but I'm not keen on recommending it as objectively better than the classic stubbing syntax. I almost prefer to not blend the classic with the DSL style. So - whenever(foo.method()).thenReturn(..) or foo.stub { on { method() } doReturn ... } but not mixing whenever with infix functions. What do you think?

Also, I know the goal is to make the whenever API take a suspendable function, but I think it's rarely needed because tests using coroutines should already be in a runTest {} block. The only case I ran into this was trying to use a mockConstruction() { mock,context -> ... } block and stubbing the mock in there.

* }
* ```
*
* This function acts as an alias for [Mockito.when], as `when` is a keyword in kotlin and as such
Copy link
Contributor

@jselbo jselbo Dec 17, 2025

Choose a reason for hiding this comment

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

Personally I find some of these new docs are too wordy. I think the code examples are good though.

@m-koops
Copy link
Contributor Author

m-koops commented Dec 17, 2025

Thank you for reviewing and providing feedback. Really much appreciated.

I did not have any intention to break any existing usages of whenever by adding inline/reified signature. I was under the impression that making the function inline/reified was improving Kotlin's capabilities to deduct the type T from the context on the call-side. But your experience is the contrary, so let me try to fix that. For now, the quick fix is to revert the change for the whenever function. I will commit the change in a minute.
Do you experience problems with any other inline/reified function? Let's fix that as well.

Then talking about lamda's versus clasic API. Actually the lambda based whenever has been the result of renaming the wheneverBlocking unifying both sync and supend function stubbing with 1 function, e.g. whenever. To me it feels odd that the user should be aware of non-standard named function for special case stubbing. I would prefer that Mockito-Kotlin would provide the stubbing API as much as possible with the function names whenever/on, the 2 names being like an alias to eachother. When I was documenting the KDocs and I was including the first code fragments, I was wandering which style to adhere to in the unit tests. I followed my personal preference, over time I'm more and more appreciating the DSL-like language features of Kotlin. But I'm happy to stick more to the clasic syntax. Shall I better do so?
In the end it would be good to have some more wiki documentation to describe the different possibilities.

Wrapping a full test body in runTest { ... } is definitely an option, but it appears cleaner to me for the tests in this repo to show that the stubbing API is not so much depending on that. In the unit tests I've tried to limit the scope of runBlocking/runTest to just the 'when/act' section of the test case. I even foresee scenarios where a unit test as a whole is without runTest/runBlocking, when certain mainline code is launching a coroutine behind the scenes of a sync function call, and the stubbing API is allowing to specify the behavior of mocks in suspend lamdas. How about that?

About the wordy documentation, sure I'm happy to shorten it. I will give it a try, but English is not my mother tongue. Is it yours?

@m-koops
Copy link
Contributor Author

m-koops commented Dec 17, 2025

Let me also share, that I have not so much of real-world unit tests available in which mocks, suspend functions and/or value classes are applied. So I'm really happy that you share your experience with snapshot builds while doing testdrives against existing suits of test code.

@jselbo
Copy link
Contributor

jselbo commented Dec 17, 2025

Do you experience problems with any other inline/reified function? Let's fix that as well.

I saw the same problem with doReturn

But I'm happy to stick more to the clasic syntax. Shall I better do so

I'm totally fine with your changes to prefer DSL syntax in the unit tests. My opinions are more about the public facing documentation and wikis.

To be clear I support this PR and making whenever support more cases like suspendable funs. I am just nitpicking some details.

About the wordy documentation, sure I'm happy to shorten it

Thanks, it doesn't have to be perfect. But I think most docs can just refer to "corresponding mockito core API" instead of repeating details.

@m-koops
Copy link
Contributor Author

m-koops commented Dec 17, 2025

I've reverted the infix/reified bit on the 2 functions mentioned. I hope this will make your test suites run again as before.

And I have been taking out significant bits of the comments.

Please have another look. Thanks.

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.

3 participants