mockk icon indicating copy to clipboard operation
mockk copied to clipboard

Issue with mocking value classes with coEvery

Open Syex opened this issue 1 year ago • 18 comments

Prerequisites

Please answer the following questions for yourself before submitting an issue.

  • [x] I am running the latest version
  • [x] I checked the documentation and found no answer
  • [x] I checked to make sure that this issue has not already been filed

Failure Information (for bugs)

I'm running into an edge case issue with value classes being used as mock answers for suspending methods that implement an interface with generics.

Consider the following interface with an implementing class using Kotlin Result (or any other value class)

interface Action<Params, ReturnType> {

    suspend fun execute(params: Params): ReturnType
}

class ResultTest : Action<Unit, Result<String>> {

    override suspend fun execute(params: Unit): Result<String> {
        TODO("Not yet implemented")
    }
}

and a simple test like

private val resultTest = mockk<ResultTest>()

@Test
fun test() = runTest {
    coEvery { resultTest.execute(Unit) } returns Result.success("abc")

    val result = resultTest.execute(Unit)
    assertTrue(result.isSuccess)
}

it fails with

class java.lang.String cannot be cast to class kotlin.Result

when calling resultTest.execute(Unit).


With a modification of the return type of the interface method execute from ReturnType to Result<ReturnType>:

interface Action<Params, ReturnType> {

    suspend fun execute(params: Params): Result<ReturnType>
}

class ResultTest : Action<Unit, String> {

    override suspend fun execute(params: Unit): Result<String> {
        TODO("Not yet implemented")
    }
}

the same test will pass.

The test will also pass with the first version of the interface if I remove the suspend keyword and replace coEvery with every.

Context

Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions.

  • MockK version: 1.13.4
  • Kotlin version: 1.8.10
  • JDK version: 11
  • JUnit version: 5.9.2
  • Type of test: unit test OR android instrumented test: Unit

Stack trace

class java.lang.String cannot be cast to class kotlin.Result (java.lang.String is in module java.base of loader 'bootstrap'; kotlin.Result is in unnamed module of loader 'app')
java.lang.ClassCastException: class java.lang.String cannot be cast to class kotlin.Result (java.lang.String is in module java.base of loader 'bootstrap'; kotlin.Result is in unnamed module of loader 'app')
	at de.memorian.wearos.marsrover.domain.action.ResultTestB.getResult-IoAF18A(MockkResultTest.kt:60)
	at de.memorian.wearos.marsrover.domain.action.MockkResultTest$test$1.invokeSuspend(MockkResultTest.kt:21)

Syex avatar Apr 01 '23 09:04 Syex

I'm facing the same issue with Kotlin 1.8.0+ (1.8.20 as well). Here is an even shorter example:

@JvmInline
value class Data(val v: Long)

interface Producer { suspend fun produce(): Data }

@Test
fun test() = runTest {
    val producer = mockk<Producer>()
    coEvery { producer.produce() } returns Data(123L)
    val result = producer.produce() // fails here
    result shouldBe Data(123L)
}

Removing the suspend modifier for produce() fixes the issue.

boiler23 avatar Apr 07 '23 16:04 boiler23

Facing the same issue, using functional interfaces (#1089). Removing the suspend modifier does not fix the issue. Not sure if it's the same underlying cause.

Wrakor avatar Apr 27 '23 16:04 Wrakor

Basically facing the same issue as @Wrakor for a quite long time and it is very annoying.

More example showing the issue can be found in my project's test class.

From my experience, the combination below fails when mocking:

  • fun interface
  • suspend
  • kotlin.Result

However, in commercial projects where I use fun interface, suspend and this kotlin-result external library, it does not happen at all.

krzdabrowski avatar May 25 '23 19:05 krzdabrowski

Any updates?

There is something really lacking here.

I've noticed that If I have the following UseCase:

fun interface GetNearbyPostalCodesUseCase {
    suspend operator fun invoke(address: Address?): Result<List<PostalCode>>
}

This mock works without any issue: coEvery { getNearbyPostalCodesUseCase(null) } returns Result.success(mockPostalCodes)

But if I rewrite the same interface as fun interface GetNearbyPostalCodesUseCase : suspend (Address?) -> Result<List<PostalCode>>

It will have the error mentioned at the start of this thread.

java.lang.ClassCastException: class java.util.Collections$SingletonList cannot be cast to class kotlin.Result (java.util.Collections$SingletonList is in module java.base of loader 'bootstrap'; kotlin.Result is in unnamed module of loader 'app')

Wrakor avatar Sep 21 '23 15:09 Wrakor

Are there any updates? The issue prevents us from designing clean APIs, as we have to adjust them to avoid value classes in return types. This isn't very pleasant :(

boiler23 avatar Dec 28 '23 15:12 boiler23

I struggled with this as well, until I realized that, since a fun interface is at its heart still an interface, it's not something that needs to be mocked; a fake object can be used more easily.

For example, in the example above:

fun interface GetNearbyPostalCodesUseCase : suspend (Address?) -> Result<List<PostalCode>>

we can simply define the following in our test:

val getNearbyPostalCodesUseCase = GetNearbyPostalCodesUseCase {
    Result.success(mockPostalCodes)
}

and call getNearbyPostalCodesUseCase as a function wherever needed.

abcarrell avatar Jan 23 '24 18:01 abcarrell

I struggled with this as well, until I realized that, since a fun interface is at its heart still an interface, it's not something that needs to be mocked; a fake object can be used more easily.

For example, in the example above:

fun interface GetNearbyPostalCodesUseCase : suspend (Address?) -> Result<List<PostalCode>>

we can simply define the following in our test:

val getNearbyPostalCodesUseCase = GetNearbyPostalCodesUseCase {
    Result.success(mockPostalCodes)
}

and call getNearbyPostalCodesUseCase as a function wherever needed.

Yes, you can manually define the use case, but that would be a workaround of the underlying issue, which is not being able to mock with coEvery as you do for every other case.

Wrakor avatar Jan 23 '24 20:01 Wrakor

Any updates on this?

aaaleem avatar Mar 13 '24 23:03 aaaleem

@Raibaz will you be able to take a look on this issue or at least point me into the direction where to fix it? It's a real blocker for my team in order to finish the migration from the Mockito and I would love to help to solve it. Thank you and appreciate your help! 🙏

nick-titov-sw avatar Aug 09 '24 15:08 nick-titov-sw

While I think @abcarrell's reasoning is correct and mocking interfaces is generally not a good idea, I understand the need to fix this issue: perhaps looking into the changes that were introduced in #1253 and around that part of the codebase could help.

Raibaz avatar Aug 13 '24 14:08 Raibaz