Improving stubbing support for suspend functions, by introducing CoroutineAwareAnswer #553
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR provides a fix for #458 and will prevent future confusion of users around the stubbing of suspend functions.
This PR also does the groundwork for #492 (support for unsigned integers), as the unsigned integers are a flavor of value classes within Kotlin.
This PR superseeds PR #552, as the change in this new PR serves more user scenarios (both ongoing and reverse stubbing) with less complexity. Therefore, I will close #552.
===
In various attemps mocking support for coroutines was introduced in Mockito-kotlin.
Problem with the current implementation is that the user should be keen to use the correct answer method (the ones that apply
SuspendableAnswer) to have Mockito work properly with suspend function. Incidentally using the non-suspendable flavour of the answer methods in Mockito-kotlin can lead to confusing error, e.g. null values and cast exceptions.To get suspend functions with a value class return value stubbed properly, one needs to apply
SuspendableAnswerand include a rather 'magic'delay(1)within the answer suspend lambda to get value class instances properly boxed when they are returned from the mock by a stubbed function call.I was hinted of all this by the comment of @Lingviston (#456 (comment)). He explains that the value class answer is properly boxed when you take a specific approach:
It is really nice that this particular solution has been found and shared in the above metioned ticket, but users of Mockito-kotlin need to be aware of this rather magic implementation quirk to get stubbing of suspend functions work properly with value classes.
This PR takes stubbing support for coroutines to a next level. In the Mockito-kotlin stubbing functions the Mockito answers are all wrapped in the new
CoroutineAwareAnswer. When the answer method of thisCoroutineAwareAnsweris called by Mockito, then this wrapping answer class will first determine whether the answer is being tied to a suspend function call. If so, theCoroutineAwareAnswerwill take care of the following:delay(1)magic to enforce suspension (leading to proper boxing of value class types)In case the
CoroutineAwareAnsweris being tied to a synchronous method/function call it will just forward its invocation to the nested delegate answer (the original Mockito answer).As long as the user is applying the Mockito-kotlin stubbing functions, this library should now take care of dealing with all intricacies related to suspend functions and value classes. For example, a user can now specify the value class return value of a stubbed suspend function call by simply applying
doReturn(valueClass).The stubbing functions that already existed, are:
doReturn(t: T)doReturn(t: T, vararg ts: T)doReturnConsecutively(ts: List<T>)doThrow(t: Throwable)doThrow( t: Throwable, vararg ts: Throwable)doThrow(t: KClass<out Throwable>)doThrow(t: KClass<out Throwable>, vararg ts: KClass<out Throwable>)doAnswer(answer: Answer<*>)doAnswer(answer: (KInvocationOnMock) -> T?)doSuspendableAnswer(answer: suspend (KInvocationOnMock) -> T?)New ones added to
OngoingStubbing, are:thenReturn(t: T)doReturnConsecutively(vararg ts: T)doCallRealMethod()callRealMethod()All these stubbing functions wrap answers in a
CoroutineAwareAnswer.The suite of unit tests have been extended with a load of new tests to cover the many scenarios to stub a function call, both suspending and non-suspending. In the
Classesfile, I have renamed theMethodstest interface toSynchronousFunctionsand I have added a new interfaceSuspendFunctionswhich combines all suspend functions used in the various tests. I have updated existing tests to let them use these 2 interfaces as much as possible. Also I have made a start to use the test naming strategy with back ticks strings starting with "should" , like`should stub function call`(). This way we should end up with better readable test names.Follow ups: