- 
                Notifications
    You must be signed in to change notification settings 
- Fork 206
Add suspend answer; Fix #346 #357
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
Conversation
ddfbbf6    to
    ecce30d      
    Compare
  
    | Thanks! Looks like a useful feature 🙌 I'm not that familiar with coroutines yet, does this solution rely on any undocumented Kotlin internals? I.e. is there any chance this may break in upcoming Kotlin versions, and if so why? | 
| Yes, it is. I am using it for some tests; otherwise, I was not able to write at all. 
 I would say yes. Implementation details are not documented well. 
 I believe this feature is quite safe: 
 I suppose there is a little chance the riskiest line could not compile with future version. | 
| Created an issue: https://youtrack.jetbrains.com/issue/KT-33766 | 
| @nhaarman, friendly ping. Any news for this solution? At Vinted we are using this solution for multiple cases and they are working well. | 
| Thanks to Roman Elizarov. He provided me with missing details and I was able to find a solution without Java. Also added some tests to make sure the cases I am facing in my personal code will be covered as well. | 
| //TODO: on coroutines >1.3 it should be job.complete() | ||
| job.cancel() | 
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.
Maybe bump version of coroutines to latest version while at it and make this change. I'm confident the tests will pass as I've done the update on another branch earlier.
@nhaarman What do you think?
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.
Otherwise really nice job @neworld. Super simple implementation and the use case for this has been increasingly in demand since Android started supporting coroutines and since coroutines-test appeared.
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.
Yeah I think this would be a good choice. Let's update to the latest supported syntax, which in this case is job.complete() as I understand it? (Not an expert here, so let me know if it has changed in the mean time)
| return thenAnswer(answer) | ||
| } | ||
|  | ||
| infix fun <T> OngoingStubbing<T>.willAnswer(answer: suspend (InvocationOnMock) -> T?): OngoingStubbing<T> { | 
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.
Thoughts on naming this doAnswer? The compiler will consider it as a valid overload I believe.
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 believe the willAnswer variants are extensions for BDDMyOngoingStubbing. May I request that we also add suspend support for BDDMyOngoingStubbing? 🙂
The tests would look something like:
...
given(fixture.suspendingWithArg(any())).willAnswer {
    withContext(Dispatchers.Default) { it.getArgument<Int>(0) }
}
...Thank you!
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.
@TimvdLippe what do you think about this request? I could create willSuspendableAnswer for BDD as well.
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.
We currently do not have any extensions for BDDMyOngoingStubbing in the existing OngoingStubbing.kt. I think we should treat that as a separate improvement, but I would happy to approve a PR on that. For now, let's keep that separate and merge this PR as-is.
| import org.mockito.stubbing.OngoingStubbing | ||
| import kotlin.DeprecationLevel.ERROR | ||
| import kotlin.coroutines.Continuation | ||
| import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn | 
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 believe this has now been moved to an *.experimental.* package in the latest version of Kotlin.
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.
It looks like it was moved back again: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.intrinsics/start-coroutine-unintercepted-or-return.html
I've tested this code on Kotlin 1.4.0 and it seems to still work.
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.
Yes. The same code was working for all coroutine and kotlin versions. Now I am using 1.4.0 with 1.3.9 of coroutines.
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.
You might consider replacing startCoroutineUninterceptedOrReturn with it's implementation if the definition location isn't very stable. All it does is cast to expose the continuation parameter and invoke.
(answer as ((InvocationOnMock, Continuation<T?>) -> Any)).invoke(it, continuation)
| @nhaarman Adding to the friendly pinging. Code like this is fundamentally difficult to test without a suspending answer: If I can't stub  I will add that while this PR uses low-level code, it is not marked experimental (it was briefly in an experimental package at one point but quickly moved back) and it is in the documentation: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.intrinsics/start-coroutine-unintercepted-or-return.html It was also the approach suggested by the team lead for Kotlin libraries in the YT issue: https://youtrack.jetbrains.com/issue/KT-33766 Would be happy to help if there's any way I can assist in getting @neworld's excellent work into a mergeable state. | 
| FWIW, in case anyone feels blocked because of this limitation, I have actually begun testing without Mockito whenever possible by leveraging interfaces and this "fake" constructor pattern I have seen being used in many well known Kotlin libraries including the Kotlinx Coroutines library. Using the  interface Api {
    suspend fetchData(): Data
    companion object {
        operator fun invoke(... /* dependencies */): Api {
            return ApiImpl(...)
        }
    }
}
private class ApiImpl(... /* dependencies */) {
    override suspend fetchData(): Data {
        // Implementation details here
    }
}Or if you don't like the  @Suppress("FunctionName")
fun Api(... /* dependencies */): Api {
    return ApiImpl(...)
}
interface Api {
    suspend fetchData(): Data
}
private class ApiImpl(... /* dependencies */) {
    override suspend fetchData(): Data {
        // Implementation details here
    }
}In code, I can still "instantiate"      private val deferredData = CompletableDeferred<Data>()
    private val api = TestApi()
    ...
    private inner class TestApi() : Api {
        override suspend fetchData(): Data {
            return deferredData.await()
        }
    }If I want to test the scenario where the fetch suspends indefinitely, I simply avoid calling  I get that this doesn't actually address the need for supporting suspend Answers in Mockito Kotlin but I figured I'd share how I have adapted my testing practices to some of the limitations. 🙂 Examples of this "fake" constructor pattern: Notice how these are actually  | 
| Actually you do not need to wait for the release. Thanks to kotlin extensions you could just copy-paste into your project: /**
 * This should be replaced after the release of https://github.com/nhaarman/mockito-kotlin/pull/357
 */
@Suppress("UNCHECKED_CAST")
infix fun <T> OngoingStubbing<T>.willAnswer(answer: suspend (InvocationOnMock) -> T?): OngoingStubbing<T> {
    return thenAnswer {
        //all suspend functions/lambdas has Continuation as the last argument.
        //InvocationOnMock does not see last argument
        val rawInvocation = it as InterceptedInvocation
        val continuation = rawInvocation.rawArguments.last() as Continuation<T?>
        answer.startCoroutineUninterceptedOrReturn(it, continuation)
    }
}I am using the same snippet for a year and works perfectly. | 
| @mhernand40 @neworld All good advice! I'm aware that I can just roll my own test doubles or copy in the extension function from this PR. (In fact, in a lot of ways I prefer not using a mocking framework at all, but I've never found a team that agrees with me. xD) I'm just trying to emphasize that if someone is trying to write tests using mockito-kotlin and runs into this issue that it's a substantial stumbling block for them without having to resort to a different approach entirely. Also, as an aside I had never seen that pattern of essentially adding a default constructor to an interface before. That's pretty neat. I came across this mostly because I was going to give a small presentation to the Android devs at my company about the differences between using Mockito + mockito-kotlin vs MockK and this stuck out as a substantial thing that MockK can do much more easily. | 
| To be honest, recently I am trying to avoid mocking my own code at all. The topic was discussed many times and there is plenty of good arguments. The exception is for 3rd party libraries I have no control. | 
| @neworld Without getting too much into the weeds, I think the correct position is between those two extremes, but I agree the far more common mistake is over-mocking. Over-mocking is a cancer. | 
| This PR has been open for a long time. The approach also seems quite stable, and would be a pretty useful addition. Is there a substantial reason not to bring it in? | 
| @nhaarman, friendly ping. More than a year has passed. Is any blocker for this PR? It seems quite useful for the community and the same code is working like a charm in our production environment. | 
| The mockito-kotlin artifact recently moved to the GitHub Mockito organization and we have been busy at work with publishing to Maven Central. Now that the dust mostly has settled, we can take a look at community PRs 😄 I will put this on my review list for this week, but I will have to get accustomed to the code a bit to understand what is going on here. From a first glance this looks fine, but I just want to make sure I understand what the overall logic is. | 
| @neworld Do you mind rebasing this PR so that I can review a clean integration? I think the only blockers are import updates. | 
ce055f4    to
    6172d09      
    Compare
  
    | Finished rebasing. 
 Suspend function under the hood returns          //all suspend functions/lambdas has Continuation as the last argument.
        //InvocationOnMock does not see last argument
        val rawInvocation = it as InterceptedInvocation
        val continuation = rawInvocation.rawArguments.last() as Continuation<T?>
        answer.startCoroutineUninterceptedOrReturn(it, continuation)More professional explanation is here: https://youtrack.jetbrains.com/issue/KT-33766#focus=Comments-27-3707299.0-0 | 
6172d09    to
    1d04c45      
    Compare
  
    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.
Sorry for the long wait. I took a look at the PR and additional context and this solution makes sense to me. There were only 2 nits that would be great to be addressed and after that we can merge this.
Thanks @neworld for driving this forward and all others who have chimed in. Once this PR lands and I got through the rest of the PRs, I will publish a new version to Maven Central.
| return thenAnswer(answer) | ||
| } | ||
|  | ||
| infix fun <T> OngoingStubbing<T>.willAnswer(answer: suspend (InvocationOnMock) -> T?): OngoingStubbing<T> { | 
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.
The willX notation is already used in BDD-style mocking. To keep the naming consistent with the other methods defined, WDYT about the following: doAnswerForCoroutine?
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.
doAnswerForCoroutine sounds weird to me, but consistency is a first-class citizen always. Just maybe we should call it doSuspendableAnswer? It will sound like on loadData do suspendable answer
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.
doSuspendableAnswer sounds good to me.
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.
Final touched done. I leave doSuspendableAnswer name
| //TODO: on coroutines >1.3 it should be job.complete() | ||
| job.cancel() | 
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.
Yeah I think this would be a good choice. Let's update to the latest supported syntax, which in this case is job.complete() as I understand it? (Not an expert here, so let me know if it has changed in the mean time)
3119f99    to
    4a04176      
    Compare
  
    This code was inspired by: - Java wrapper part: https://github.com/square/retrofit/blob/8c93b59dbc57841959f5237cb141ce0b3c18b778/retrofit/src/main/java/retrofit2/HttpServiceMethod.java#L168 - Kotlin wrapper part: https://github.com/square/retrofit/blob/8c93b59dbc57841959f5237cb141ce0b3c18b778/retrofit/src/main/java/retrofit2/KotlinExtensions.kt#L83-L98
4a04176    to
    cd635be      
    Compare
  
    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.
Thank you so much for your continued effort and apologies that it took so long! I will make sure this change will be published to Maven Central after a successful build.
| This feature is available per version 3.1.0: https://repo1.maven.org/maven2/org/mockito/kotlin/mockito-kotlin/3.1.0/ | 
This code was inspired by:
I used Java code because kotlin does not allow call suspend functions or lambdas by giving own Continuation. But Kotlin does it under the hood, so Java could do it as well. For example: