Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking extensions from Continuation

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))
like image 668
m0skit0 Avatar asked Jan 26 '23 00:01

m0skit0


1 Answers

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:

  1. Mocking inline function is generally impossible, because they are inlined at compile time and mocking runs later at runtime. Mock functions, which are called inside inlined function instead.
  2. When dealing with inline classes, match inlined value, not the wrapper. So instead of 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.

like image 132
esentsov Avatar answered Mar 10 '23 22:03

esentsov