Continuation 的模拟扩展

Mocking extensions from Continuation

我想从标准库 Continuation class 中模拟 resumeresumeWithException。都是扩展函数。

这是我的 JUnit 设置函数:

@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
}

但是这不起作用,在 resumeWithException 函数的模拟中抛出以下异常:

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.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.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access[=11=]0(ParentRunner.java:58)
    at org.junit.runners.ParentRunner.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)

这是 resumeWithException 的代码,与 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))

这里有个小调查。如果您只是在寻找内联函数和内联 classes 的解决方案,请滚动到解决方案部分。

详细解释:

这些是现代科特林功能的棘手后果。让我们在 kotlin 插件的帮助下 将此代码反编译 为 java。 这个mockContinuation.resumeWithException(any())变成这个样子(精简美化版)

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);

如您所见,发生了一些事情。首先,不再调用 resumeWithException。因为它是一个内联函数,它被编译器内联了,所以现在它是一个 resumeWith 调用。其次,由 any() 编辑的匹配器 return 被一个神秘的调用包裹 kotlin.Result.constructor-impl(ResultKt.createFailure(anyThrowable)),它不是在模拟上调用的函数的参数。这就是为什么 mockk 无法匹配签名和匹配器的原因。 显然我们可以尝试通过模拟 resumeWith 函数本身来修复它:

every { mockContinuation.resumeWith(any()) } just Runs

而且也不行!这是反编译的代码:

Matcher matcher = (new ConstantMatcher(true));
Object anyValue = getCallRecorder().matcher(matcher, Reflection.getOrCreateKotlinClass(kotlin.Result.class));
mockContinuation.resumeWith(((kotlin.Result)anyValue).unbox-impl());

还有一个神秘的电话unbox-impl()。来看看Resultclass定义

public inline class Result<out T> @PublishedApi internal constructor(
    @PublishedApi
    internal val value: Any?
) 

这是一个内联class! ubox-impl() 是编译器生成的函数,如下所示:

public final Object unbox-impl() {
   return this.value;
}

基本上,编译器通过将其替换为 value 来内联 Result 对象。 所以再一次,我们最后没有调用 resumeWith(any()) 而调用 resumeWith(any().value) 并且模拟库很混乱。 那么如何模拟呢?请记住,mockContinuation.resume(any()) 出于某种原因起作用,即使 resume 只是另一个内联函数

public inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))

反编译mockContinuation.resume(any())给我们

Object anyValue = getCallRecorder().matcher(matcher, Reflection.getOrCreateKotlinClass(Unit.class));
Object result = kotlin.Result.constructor-impl(anyValue);
mockContinuation.resumeWith(result);

正如我们所见,它确实是内联的,resumeWith 是用 result 对象调用的,而不是 anyValue,这是我们的匹配器。但是,让我们来看看这个神秘的kotlin.Result.constructor-impl:

public static Object constructor-impl(Object value) {
      return value;
}

所以它实际上并没有包装值,只是 return 它!这就是为什么它实际上有效并为我们提供了如何模拟 resumeWith:

的解决方案
     every { mockContinuation.resumeWith(Result.success(any())) } just Runs

是的,我们正在将我们的匹配器包装到 Result 中,如我们所见,它被内联。但是如果我们想区分Result.success()Result.failure()呢?我们仍然不能 mock mockContinuation.resumeWith(Result.failure(any())),因为 failure() 调用将参数包装成其他东西(查看上面的源代码或反编译代码)。 所以我可以考虑类似的事情:

     every { mockContinuation.resumeWith(Result.success(any())) } answers {
            val result = arg<Any>(0)
            if (result is Unit) {
                println("success")
            } else {
                println("fail")
            }
        }

result 值是我们的类型(在本例中为 Unit)或 Result.Failure 类型的实例,后者是内部类型。

解法:

  1. 模拟内联函数通常是不可能的,因为它们是在编译时内联的,模拟在运行时稍后运行。模拟函数,而是在内联函数中调用。
  2. 处理内联 classes 时,匹配内联值,而不是包装器。因此,使用 mock.testFunction(InlinedClass(any<Value>())) 而不是 mock.testFunction(any<InlinedClass>())

Heremockk 支持内联 classes 的功能请求,目前处于打开状态。