Проблемы с Kotlin Result<T> в модульных тестах

Я работаю над приложением для Android и решил использовать класс Kotlin Result, чтобы справиться с успехом/неудачей в моих операциях. Я внес изменения в код, но тесты перестают работать, и я не могу понять, почему. Здесь я покажу вам несколько фрагментов:

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())
}

И это исключение, которое я получаю прямо сейчас. Любая подсказка? Похоже, что метод fold() не выполняется при модульном тестировании.

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

    at NetworkDataSource.getItems(NetworkDataSource.kt:31)

Проблема не в вашем модульном тесте, а в коде в NetworkDataSource.kt. Я не знаком с Kotlin для Android, но нет функции fold, которая получает 2 функции - kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/…

Augusto 23.12.2020 10:28

Можете показать, какая у вас постоянная MOCK_ITEMS_DOCUMENT?

ChristianB 23.12.2020 11:11
6
2
4 472
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

У меня была такая же проблема.

Я заметил, что мой метод внедренного класса, который должен возвращать Result<List<Any>>, на самом деле возвращает Result<Result<List<Any>>>, что вызывает ClassCastException. Я использовал опцию Evaluate Expression для результата метода, и я получил

Success(Success([]))

Приложение работает хорошо, но модульные тесты не прошли из-за этой проблемы.

В качестве временного решения я создал новую простую реализацию Result запечатанного класса с fold() функцией расширения. В будущем должно быть легко заменить на kotlin.Result

Result закрытый класс:

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

fold() функция расширения:

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)
}

Ты прав, Патрик, это то, что я переживал. Спасибо за вашу помощь. Я обнаружил эту проблему в проекте Kotlin. Это будет решено 1.4.30.

Jose Maria Payá Castillo 29.12.2020 16:24

Я нашел другой обходной путь для этой проблемы с переносом результатов для тех, кто не хочет создавать свой собственный тип Result.

Эта проблема возникает именно при использовании функций Mockito .thenReturn on suspend. Я обнаружил, что использование .thenAnswer не вызывает проблем.

Поэтому вместо того, чтобы писать это в своем модульном тесте (изменено doReturn на thenReturn здесь):

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

Использовать:

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

Обновлено: я должен отметить, что я все еще сталкивался с этой проблемой при запуске Kotlin 1.5.0.

Обновлено: в Kotlin 1.5.20 я снова могу использовать .thenReturn.

После глубокого изучения проблемы я, наконец, нашел временное решение, которое работает в тестовой среде. Проблема в том, что каким-то образом значение объекта Result обернуто другим Result, и мы можем получить желаемое значение или исключение, используя отражение.

Итак, я создал функцию расширения под названием mockSafeFold, которая реализует поведение fold в обычных вызовах и отлично работает при выполнении модульных тестов.

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())
}

Затем просто назовите его вместо fold:

val result: Result = myUseCase(param)

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

Другие вопросы по теме