mockk: java.lang.ClassCastException: kotlin.Result cannot be cast to java.util.List
Prerequisites
Please answer the following questions for yourself before submitting an issue.
- I am running the latest version
- I checked the documentation and found no answer
- I checked to make sure that this issue has not already been filed
Expected Behavior
I’m testing a class that has a suspend fun
and returns Result<List<..>>
. In a unit test I’m mocking this class and want to return a Result.success(...)
from that method. Expecting that my mock returns that value when called, but it crashes instead.
First I thought it’s a Kotlin bug and wanted to file an issue there, however, in production and when removing the mockk calls, the code works as expected, so I assume it’s instead a mockk bug.
This sample code snippet:
class ExampleUnitTest {
private val testClass = mockk<TestClass>()
@Test
fun `test bug`() = runBlockingTest {
coEvery { testClass.execute() } returns Result.success(listOf("1"))
testClass.execute()
.fold(
onSuccess = {
println(it)
},
onFailure = {
println("fail")
}
)
}
}
class TestClass {
suspend fun execute(): Result<List<String>> {
return suspendCoroutine {
it.resume(Result.success(listOf("")))
}
}
}
crashes with
java.lang.ClassCastException: kotlin.Result cannot be cast to java.util.List
at de.syex.resultbug.ExampleUnitTest$test bug$1.invokeSuspend(ExampleUnitTest.kt:24)
at de.syex.resultbug.ExampleUnitTest$test bug$1.invoke(ExampleUnitTest.kt)
at kotlinx.coroutines.test.TestBuildersKt$runBlockingTest$deferred$1.invokeSuspend(TestBuilders.kt:50)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50)
...
The offending line 24 is the fold()
. I found this issue while testing some code that actually runs fine. Instantiating TestClass
instead
private val testClass = TestClass()
lets the test pass as expected.
- MockK version: 1.10.0
- Kotlin version: 1.4.0
- Coroutine version: 1.3.9
- JDK version: 1.8
- JUnit version: 4.12 & 5.6.2
- Gradle version: 6.1.1 & 6.5.1
- Type of test: unit test
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 20
- Comments: 18 (1 by maintainers)
Request to re-open, this is an actual bug caused by inline classes like Result. Take a look at the first comment on https://youtrack.jetbrains.com/issue/KT-30223. I’m not sure this will be fixed any time soon. Anyone know of a workaround?
I found this same issue with a Retrofit
CallAdapter
forResult
, I seen it when callinggetOrThrow()
.The workaround:
Although I named it
safeGetOrThrow()
to differentiate from the stdlib version.The problem is that sometimes (not sure why), a
Result<T>
instance (especially from a RetrofitCallAdapter
forResult
) can contain avalue
of a boxedResult<T>
, but internally ingetOrThrow()
it assumes that if thevalue
isn’t aResult.Failure
, it unsafe casts it toT
, resulting in theClassCastException
:You can use this function to reproduce the issue reliably:
If you decompile that back to Java, you can see the nested
Result
instances being boxed, where calling the stdlibgetOrThrow()
will result in the sameClassCastException
, but the fixed version will not.I have reported it straight to the Kotlin team: https://youtrack.jetbrains.com/issue/KT-46477#focus=Comments-27-4952485.0-0 Because it is a language-level bug.
I’ve seen the implementation that has been done in Mockito to handle this.
It seems like we can replicate it in mockk, it’s basically about trying to unbox the underlying value contained in the Result class if possible and returning it in the answerer.
However, it’s only feasible from Kotlin 1.5.0 onwards, because it requires the @JvmInline annotation to be available and set on the Result class (or any other inline class, for that matter).
Given the fact that 1.5.0 was released very recently and using Result as a return type is not possible by default but it requires the
-Xallow-result-return-type
kotlin option to be set, this doesn’t look to me like a high priority issue right now.Reopening it for now and leaving it here in case anyone wants to submit a PR.
Ouch, the fact that it’s being caused by the fact that Result is defined as inline makes it basically the same issue as #55.
Looks like a workaround for inline classes/functions is not really feasible, I’ll try investigating but I’m not really confident on this.