Я работаю над приложением для 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)
Можете показать, какая у вас постоянная MOCK_ITEMS_DOCUMENT?
У меня была такая же проблема.
Я заметил, что мой метод внедренного класса, который должен возвращать 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.
Я нашел другой обходной путь для этой проблемы с переносом результатов для тех, кто не хочет создавать свой собственный тип 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 */ }
)
Проблема не в вашем модульном тесте, а в коде в
NetworkDataSource.kt. Я не знаком с Kotlin для Android, но нет функцииfold, которая получает 2 функции - kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/…