来自 Continuation 的模拟扩展

m0s*_*it0 4 junit kotlin mockk

我想从标准库类中模拟resume和。两者都是扩展函数。resumeWithExceptionContinuation

这是我的 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
}
Run Code Online (Sandbox Code Playgroud)

但是这不起作用,在模拟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$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)
Run Code Online (Sandbox Code Playgroud)

这是与以下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))
Run Code Online (Sandbox Code Playgroud)

ese*_*sov 8

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

长解释

这些是现代 kotlin 功能的棘手后果。让我们在 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);
Run Code Online (Sandbox Code Playgroud)

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

every { mockContinuation.resumeWith(any()) } just Runs
Run Code Online (Sandbox Code Playgroud)

它也不起作用!这是反编译的代码:

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

这是另一个神秘的电话unbox-impl()。让我们看一下Result类定义

public inline class Result<out T> @PublishedApi internal constructor(
    @PublishedApi
    internal val value: Any?
) 
Run Code Online (Sandbox Code Playgroud)

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

public final Object unbox-impl() {
   return this.value;
}
Run Code Online (Sandbox Code Playgroud)

基本上,编译器内联Result对象,将其替换为value. 因此,再次调用,而不是resumeWith(any())最后调用resumeWith(any().value),模拟库被混淆了。那么如何嘲讽呢?请记住,mockContinuation.resume(any())出于某种原因工作,即使resume只是另一个内联函数

public inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))
Run Code Online (Sandbox Code Playgroud)

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

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

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

public static Object constructor-impl(Object value) {
      return value;
}
Run Code Online (Sandbox Code Playgroud)

所以它实际上没有包装价值,只是返回它!这就是为什么它实际上有效并为我们提供了如何模拟的解决方案resumeWith

     every { mockContinuation.resumeWith(Result.success(any())) } just Runs
Run Code Online (Sandbox Code Playgroud)

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

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

result值是我们的类型(Unit在这种情况下)或Result.Failure类型的实例,这是一个内部类型。

解决方案:

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

mockk支持内联类的功能请求,目前处于 Opened 状态。