Mocking extensions from Continuation
Answer #1 100 %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 @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 Continuation.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(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:
- 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.
- When dealing with inline classes, match inlined value, not the wrapper. So instead of
mock.testFunction(any
use()) mock.testFunction(InlinedClass(any
.()))
Here is a feature request for mockk
to support inline classes, which is currently in Opened state.