I want to mock resume
and resumeWithException
from the standard library Continuation
class. Both are extension functions.
Here's my JUnit setup function:
@MockK
private lateinit var mockContinuation: Continuation<Unit>
@Before
fun setup() {
MockKAnnotations.init(this)
mockkStatic("kotlin.coroutines.ContinuationKt")
every { mockContinuation.resume(any()) } just Runs
every { mockContinuation.resumeWithException(any()) } just Runs
}
However this does not work, throwing the following exception at the mocking of resumeWithException
function:
io.mockk.MockKException: Failed matching mocking signature for
SignedCall(retValue=java.lang.Void@5b057c8c, isRetValueMock=false, retType=class java.lang.Void, self=Continuation(mockContinuation#1), method=resumeWith(Any), args=[null], invocationStr=Continuation(mockContinuation#1).resumeWith(null))
left matchers: [any()]
at io.mockk.impl.recording.SignatureMatcherDetector.detect(SignatureMatcherDetector.kt:99)
at io.mockk.impl.recording.states.RecordingState.signMatchers(RecordingState.kt:39)
at io.mockk.impl.recording.states.RecordingState.round(RecordingState.kt:31)
at io.mockk.impl.recording.CommonCallRecorder.round(CommonCallRecorder.kt:50)
at io.mockk.impl.eval.RecordedBlockEvaluator.record(RecordedBlockEvaluator.kt:59)
at io.mockk.impl.eval.EveryBlockEvaluator.every(EveryBlockEvaluator.kt:30)
at io.mockk.MockKDsl.internalEvery(API.kt:92)
at io.mockk.MockKKt.every(MockK.kt:104)
at com.blablabla.data.pair.TestConnectSDKDeviceListener.setup(TestConnectSDKDeviceListener.kt:26)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:24)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
And this is the code for resumeWithException
which is very similar to resume
:
/**
* Resumes the execution of the corresponding coroutine so that the [exception] is re-thrown right after the
* last suspension point.
*/
@SinceKotlin("1.3")
@InlineOnly
public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
resumeWith(Result.failure(exception))
Here is a small investigation. If you are just looking for solution for inline functions and inline classes, scroll to solution section.
Long explanation:
These are tricky consequences of modern kotlin features. Let's decompile this code to java with help of kotlin plugin.
This mockContinuation.resumeWithException(any())
becomes something like this (shortened and beautified version)
Matcher matcher = (new ConstantMatcher(true));
Throwable anyThrowable = (Throwable)getCallRecorder().matcher(matcher, Reflection.getOrCreateKotlinClass(Throwable.class));
Object result = kotlin.Result.constructor-impl(ResultKt.createFailure(anyThrowable));
mockContinuation.resumeWith(result);
As you can see a few things happened. First, there is no call to resumeWithException
anymore. Because it is an inline function, it was inlined by the compiler, so now it's a resumeWith
call. Second, matcher returned by any()
was wrapped with a mysterious call
kotlin.Result.constructor-impl(ResultKt.createFailure(anyThrowable))
, and it's not an argument of a function, called on the mock. That's why mockk can't match the signature and the matcher.
Obviously we can try to fix it by mocking resumeWith
function itself:
every { mockContinuation.resumeWith(any()) } just Runs
And it does not work either! Here is the decompiled code:
Matcher matcher = (new ConstantMatcher(true));
Object anyValue = getCallRecorder().matcher(matcher, Reflection.getOrCreateKotlinClass(kotlin.Result.class));
mockContinuation.resumeWith(((kotlin.Result)anyValue).unbox-impl());
And here is another mysterious call unbox-impl()
. Let's look at Result
class definition
public inline class Result<out T> @PublishedApi internal constructor(
@PublishedApi
internal val value: Any?
)
It's an inline class! And ubox-impl()
is a compiler-generated function like this:
public final Object unbox-impl() {
return this.value;
}
Basically, the compiler inlines Result
object, by replacing it with it's value
.
So again, instead of calling resumeWith(any())
in the end we call resumeWith(any().value)
and mocking library is confused.
So how to mock it? Remember, mockContinuation.resume(any())
worked for some reason, even though resume
is just another inline function
public inline fun <T> Continuation<T>.resume(value: T): Unit =
resumeWith(Result.success(value))
Decompiling mockContinuation.resume(any())
gives us
Object anyValue = getCallRecorder().matcher(matcher, Reflection.getOrCreateKotlinClass(Unit.class));
Object result = kotlin.Result.constructor-impl(anyValue);
mockContinuation.resumeWith(result);
As we can see, it was inlined indeed and resumeWith
was called with a result
object, not with anyValue
, which is our matcher. But, let's take a look at this mysterious kotlin.Result.constructor-impl
:
public static Object constructor-impl(Object value) {
return value;
}
So it actually does not wrap the value, juts return it! That's why it actually works and gives us a solution how to mock resumeWith
:
every { mockContinuation.resumeWith(Result.success(any())) } just Runs
Yes, we are wrapping our matcher into Result
, which as we saw, gets inlined. But what if we want to distinguish between Result.success()
and Result.failure()
? We still can't mock mockContinuation.resumeWith(Result.failure(any()))
, because failure()
call wraps the argument into something else (check the source code or decompiled code above).
So I can think about something like that:
every { mockContinuation.resumeWith(Result.success(any())) } answers {
val result = arg<Any>(0)
if (result is Unit) {
println("success")
} else {
println("fail")
}
}
The result
value is instance of either our type (Unit
in this case) or Result.Failure
type, which is an internal type.
Solution:
mock.testFunction(any<InlinedClass>())
use mock.testFunction(InlinedClass(any<Value>()))
. Here is a feature request for mockk
to support inline classes, which is currently in Opened state.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With