Problems with Kotlin Result<T> on unit tests

Viewed 3060

I am working on an android Application and I opted to use Kotlin Result class so as to handle success/failure on my operations. I made the changes to the code, but the tests stop working and I cannot understand why. Here I show you some snippets:

FireStoreClient.kt

suspend fun items(): Result<ItemsResponse>

NetworkDataSource.kt

suspend fun getItems(): List<Item> =
    fireStoreClient.items().fold({ it.items.map { item -> item.toDomain() } }, { emptyList() })

NetworkDataSourceTest.kt

@ExperimentalCoroutinesApi
@Test
fun `Check getItems works properly`() = runBlockingTest {
    whenever(fireStoreClient.items()).doReturn(success(MOCK_ITEMS_DOCUMENT))
    val expectedResult = listOf(
        Item(
            id = 1,
            desc = "Description 1"
        ),
        Item(
            id = 2,
            desc = "Description 2"
        )
    )
    assertEquals(expectedResult, dataSource.getItems())
}

And this is the exception I am getting right now. Any clue? It appears that the fold() method is not being executed when unit testing.

java.lang.ClassCastException: kotlin.Result cannot be cast to ItemsResponse

    at NetworkDataSource.getItems(NetworkDataSource.kt:31)
3 Answers

I've found a different workaround for this result-wrapping issue, for those who don't want to make their own Result type.

This issue appears to happens specifically when using Mockito's .thenReturn on suspend functions. I've found that using .thenAnswer doesn't exhibit the problem.

So instead of writing this in your unit test (changed doReturn to thenReturn here):

whenever(fireStoreClient.items()).thenReturn(success(MOCK_ITEMS_DOCUMENT))

Use:

whenever(fireStoreClient.items()).thenAnswer { success(MOCK_ITEMS_DOCUMENT) }

Edit: I should note that I was still experiencing this issue when running Kotlin 1.5.0.

Edit: On Kotlin 1.5.20 I can use .thenReturn again.

After a deep dive into the problem, finally, I've found a temporary workaround that works in the testing environment. The problem is, somehow the value of the Result object is wrapped by another Result, and we can pull the desired value or exception using reflection.

So, I've created an extension function called mockSafeFold, which implements the fold behavior in normal calls, and acts fine when you are executing unit-tests.

inline fun <R, reified T> Result<T>.mockSafeFold(
    onSuccess: (value: T) -> R,
    onFailure: (exception: Throwable) -> R
): R = when {
    isSuccess -> {
        val value = getOrNull()
        try {
            onSuccess(value as T)
        } catch (e: ClassCastException) {
            // This block of code is only executed in testing environment, when we are mocking a
            // function that returns a `Result` object.
            val valueNotNull = value!!
            if ((value as Result<*>).isSuccess) {
                valueNotNull::class.java.getDeclaredField("value").let {
                    it.isAccessible = true
                    it.get(value) as T
                }.let(onSuccess)
            } else {
                valueNotNull::class.java.getDeclaredField("value").let {
                    it.isAccessible = true
                    it.get(value)
                }.let { failure ->
                    failure!!::class.java.getDeclaredField("exception").let {
                        it.isAccessible = true
                        it.get(failure) as Exception
                    }
                }.let(onFailure)
            }
        }
    }
    else -> onFailure(exceptionOrNull() ?: Exception())
}

Then, simply call it instead of fold:

val result: Result = myUseCase(param)

result.mockSafeFold(
    onSuccess = { /* do whatever */ },
    onFailure = { /* do whatever */ }
)

I had the same issue.

I noticed that my method of injected class which should return Result<List<Any>> returns actually Result<Result<List<Any>>> which causes the ClassCastException. I used the Evaluate Expression option for the result from the method and I got

Success(Success([]))

The app works well but unit tests didn't pass due this problem.

As a temporary solution I built a new simple implementation of Result sealed class with fold() extension function. It should be easy to replace in future to kotlin.Result

Result sealed class:

sealed class Result<T> {
    data class Success<T>(val value: T) : Result<T>()
    data class Failure<T>(val error: Throwable) : Result<T>()
}

fold() extension function:

inline fun <R, T> Result<T>.fold(
    onSuccess: (value: T) -> R,
    onFailure: (exception: Throwable) -> R
): R = when (this) {
    is Result.Success -> onSuccess(value)
    is Result.Failure -> onFailure(error)
}
Related