Continuation 的模拟扩展
Mocking extensions from Continuation
我想从标准库 Continuation
class 中模拟 resume
和 resumeWithException
。都是扩展函数。
这是我的 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()
。来看看Result
class定义
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
类型的实例,后者是内部类型。
解法:
- 模拟内联函数通常是不可能的,因为它们是在编译时内联的,模拟在运行时稍后运行。模拟函数,而是在内联函数中调用。
- 处理内联 classes 时,匹配内联值,而不是包装器。因此,使用
mock.testFunction(InlinedClass(any<Value>()))
而不是 mock.testFunction(any<InlinedClass>())
。
Here 是 mockk
支持内联 classes 的功能请求,目前处于打开状态。
我想从标准库 Continuation
class 中模拟 resume
和 resumeWithException
。都是扩展函数。
这是我的 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()
。来看看Result
class定义
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
类型的实例,后者是内部类型。
解法:
- 模拟内联函数通常是不可能的,因为它们是在编译时内联的,模拟在运行时稍后运行。模拟函数,而是在内联函数中调用。
- 处理内联 classes 时,匹配内联值,而不是包装器。因此,使用
mock.testFunction(InlinedClass(any<Value>()))
而不是mock.testFunction(any<InlinedClass>())
。
Here 是 mockk
支持内联 classes 的功能请求,目前处于打开状态。