4

我想模拟resumeresumeWithException来自标准库Continuation类。两者都是扩展功能。

这是我的 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$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)

这是与以下代码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))
4

1 回答 1

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

如您所见,发生了一些事情。首先,不再有调用resumeWithException。因为它是一个内联函数,它被编译器内联了,所以现在它是一个resumeWith调用。其次,由返回的 matcherany()被一个神秘的 call 包裹 kotlin.Result.constructor-impl(ResultKt.createFailure(anyThrowable)),它不是函数的参数,在 mock 上调用。这就是为什么 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()。让我们看看Result类定义

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

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

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

基本上,编译器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))

反编译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;
}

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

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

是的,我们将匹配器包装到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")
            }
        }

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

解决方案:

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

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

于 2019-08-07T12:25:48.107 回答