example 1]
#Test
fun `validate live data`() = runBlocking {
`when`(repository.GetList()).thenReturn(list)
val vm = TestViewModel(testUseCase, userManager)
idHashMap.keys.forEach {
vm.getList(it)
vm.listLivedata.observeForeEver {
assertEquals(
(vm.listLivedata.getOrAwaitValue() as Result.Success<List>)?.data?.listData!!::class.java,
idHashMap[it]
)
assertTrue(
vm.listLiveData.getOrAwaitValue() is Result.Success,
"error"
)
}
}
}
example 2]
#Test
fun `validate live data`() = runBlocking {
`when`(repository.GetList()).thenReturn(list)
val vm = TestViewModel(testUseCase, userManager)
idHashMap.keys.forEach {
vm.getList(it)
vm.listLivedata.observeForeEver {
assertTrue(
vm.listLiveData.getOrAwaitValue() is Result.Success,
"error"
)
}
}
}
1st example always fails the test due to asserEquals() but 2nd always passes a test(when I remove assertEqual()); I wonder what is the reason? Is calling operation that update livedata inside loop, somehow causing livedata issue?
Related
I have the below code in my view model class.
class MarketViewModel #Inject constructor(repo: MarketRepository) : ViewModel() {
private val retry = MutableStateFlow(0)
val marketState: LiveData<State<Market>> =
retry.flatMapLatest{repo.refreshMarket()}
.map { State.Success(it) as State<T> }
.catch { error -> emit(State.Error(error)) }
.stateIn(vmScope, SharingStarted.WhileSubscribed(5000), State.Loading())
.asLiveData()
fun retry() {
retry.value++
}
}
MarketRepository.kt:
fun refreshMarket() =
flow { emit(api.getMarkets()) }
.onEach { db.upsert(it) }
.flowOn(dispatchers.IO)
It works fine until a network error occurs in the repository method refreshMarket then when I call the retry() on the view model, it doesn't trigger the flatMapLatest transformer function anymore on the retry MutableStateFlow, why?
Does the flow get complete when it calls a Catch block? how to handle such situation?
You're right, catch won't continue emitting after an exception is caught. As the documentation says, it is conceptually similar to wrapping all the code above it in try. If there is a loop in a traditional try block, it does not continue iterating once something is thrown, for example:
try {
for (i in 1..10) {
if (i == 2) throw RuntimeException()
println(i)
}
} catch (e: RuntimeException) {
println("Error!")
}
In this example, once 2 is encountered, the exception is caught, but code flow does not return to the loop in the try block. You will not see any numbers printed that come after 2.
You can use retryWhen instead of catch to be able to restart the flow. To do it on demand like you want, maybe this strategy could be used (I didn't test it):
class MarketViewModel #Inject constructor(repo: MarketRepository) : ViewModel() {
private val retry = MutableSharedFlow<Unit>()
val marketState: LiveData<State<Market>> =
repo.refreshMarket()
.map { State.Success(it) as State<T> }
.retryWhen { error, _ ->
emit(State.Error(error))
retry.first() // await next value from retry flow
true
}
.stateIn(vmScope, SharingStarted.WhileSubscribed(5000), State.Loading())
.asLiveData()
fun retry() {
retry.tryEmit(Unit)
}
}
I'm trying to unit test my viewmodel code, which does a server polling to return data as soon as its Status == ELIGIBLE
My problem is, it always assert when it's still loading (repeating), and not waiting for the onSuccess to be called to assert the correct status.
I've put some logs to track what's happening:
doOnSubscribe called
repeatWhen called
doOnNext called
takeUntil called
doOnNext called
takeUntil called
As you can see, repeatWhen and takeUntil are called twice (which is expected), but after that, no onSuccess called.
And eventually the test fails with this message
Caused by: java.lang.AssertionError: expected:<SUCCESS> but was:<LOADING>
If I removed the failing line, the next assertion would fail too with message:
java.lang.AssertionError:
Expected :ELIGIBLE
Actual :null
Which mean the onSuccess method is not yet reached, and is still loading.
I also don't prefer using Schedulers.trampoline() .. it works, but it waits for 5 secs synchronously. I prefer to use TestScheduler.advanceByTime() instead.
Here's the client code:
fun startStatusPolling() {
val pollingSingle = shiftPayService.obtainCardStatus()
.repeatWhen {
println("repeatWhen called")
//POLLING_INTERVAL_SECONDS = 5
it.delay(POLLING_INTERVAL_SECONDS, TimeUnit.SECONDS)
}
.takeUntil { item ->
println("takeUntil called")
item.cardStatus != Status.PENDING
}.doOnNext {
println("doOnNext called")
}
.lastElement()
.toSingle()
subscribe(pollingSingle, pollingStatusLiveData)
}
And my test class:
#RunWith(HomebaseRobolectricTestRunner::class)
#LooperMode(LooperMode.Mode.PAUSED)
class CardViewModelTest {
lateinit var viewModel: CardViewModel
var testScheduler = TestScheduler()
#Before
fun setup() {
RxJavaPlugins.setComputationSchedulerHandler { testScheduler }
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { testScheduler }
val cardStatusPending: CardStatus = mockk(relaxed = true) {
every { status } returns Status.PENDING
}
val cardStatusEligible: CardStatus = mockk(relaxed = true) {
every { status } returns Status.ELIGIBLE
}
val cardService: CardService = spyk {
every { obtainCardStatus() } returnsMany listOf(
Single.just(cardStatusPending),
Single.just(cardStatusEligible)
)
}
viewModel = CardViewModel(cardService)
}
#Test
fun testCardStatusPolling() {
viewModel.startStatusPolling()
shadowOf(Looper.getMainLooper()).idle()
testScheduler.advanceTimeBy(5, TimeUnit.SECONDS)
//after 5 sec delay, single is resubscibed, returning the second single cardStatusEligible
assertEquals(Result.Status.SUCCESS, viewModel.pollingStatusLiveData.value?.status)
assertEquals(EligiblityStatus.ELIGIBLE, viewModel.pollingStatusLiveData.value?.data?.eligibilityStatus)
}
}
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)
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)
}
I am making a network repository that supports multiple data retrieval configs, therefore I want to separate those configs' logic into functions.
However, I have a config that fetches the data continuously at specified intervals. Everything is fine when I emit those values to the original Flow. But when I take the logic into another function and return another Flow through it, it stops caring about its coroutine scope. Even after the scope's cancelation, it keeps on fetching the data.
TLDR: Suspend function returning a flow runs forever when currentCoroutineContext is used to control its loop's termination.
What am I doing wrong here?
Here's the simplified version of my code:
Fragment calling the viewmodels function that basically calls the getData()
lifecycleScope.launch {
viewModel.getLatestDataList()
}
Repository
suspend fun getData(config: MyConfig): Flow<List<Data>>
{
return flow {
when (config)
{
CONTINUOUS ->
{
//It worked fine when fetchContinuously was ingrained to here and emitted directly to the current flow
//And now it keeps on running eternally
fetchContinuously().collect { updatedList ->
emit(updatedList)
}
}
}
}
}
//Note logic of this function is greatly reduced to keep the focus on the problem
private suspend fun fetchContinuously(): Flow<List<Data>>
{
return flow {
while (currentCoroutineContext().isActive)
{
val updatedList = fetchDataListOverNetwork().await()
if (updatedList != null)
{
emit(updatedList)
}
delay(refreshIntervalInMs)
}
Timber.i("Context is no longer active - terminating the continuous-fetch coroutine")
}
}
private suspend fun fetchDataListOverNetwork(): Deferred<List<Data>?> =
withContext(Dispatchers.IO) {
return#withContext async {
var list: List<Data>? = null
try
{
val response = apiService.getDataList().execute()
if (response.isSuccessful && response.body() != null)
{
list = response.body()!!.list
}
else
{
Timber.w("Failed to fetch data from the network database. Error body: ${response.errorBody()}, Response body: ${response.body()}")
}
}
catch (e: Exception)
{
Timber.w("Exception while trying to fetch data from the network database. Stacktrace: ${e.printStackTrace()}")
}
finally
{
return#async list
}
list //IDE is not smart enough to realize we are already returning no matter what inside of the finally block; therefore, this needs to stay here
}
}
I am not sure whether this is a solution to your problem, but you do not need to have a suspending function that returns a Flow. The lambda you are passing is a suspending function itself:
fun <T> flow(block: suspend FlowCollector<T>.() -> Unit): Flow<T> (source)
Here is an example of a flow that repeats a (GraphQl) query (simplified - without type parameters) I am using:
override fun query(query: Query,
updateIntervalMillis: Long): Flow<Result<T>> {
return flow {
// this ensures at least one query
val result: Result<T> = execute(query)
emit(result)
while (coroutineContext[Job]?.isActive == true && updateIntervalMillis > 0) {
delay(updateIntervalMillis)
val otherResult: Result<T> = execute(query)
emit(otherResult)
}
}
}
I'm not that good at Flow but I think the problem is that you are delaying only the getData() flow instead of delaying both of them.
Try adding this:
suspend fun getData(config: MyConfig): Flow<List<Data>>
{
return flow {
when (config)
{
CONTINUOUS ->
{
fetchContinuously().collect { updatedList ->
emit(updatedList)
delay(refreshIntervalInMs)
}
}
}
}
}
Take note of the delay(refreshIntervalInMs).
This function is executed from my activity to check if a specific feature flag is enable, dispatcher.io() is injected so, for test mode the dispatcher is Main
fun isFeatureFlagEnabled(nameKey: String, completion: (enabled: Boolean) -> Unit) {
CoroutineScope(context = dispatcher.io()).launch {
val featureFlag = featureFlagsRepository.featureFlagsDao.getFeatureFlag(nameKey)
if (featureFlag != null) {
completion(featureFlag.isEnabled)
}
else {
completion(false)
}
}
}
This is the invocation if the function in the activity
private fun launchClearentCardOnFileBaseOnFeatureFlag(cardNavHelper: CardNavHelper){
featureFlagsHelper?.isFeatureFlagEnabled(CLEARENT_CARD_ON_FILE){enable ->
if(enable){
putFragment(
ClearentNativeFormFragment.newInstance(cardNavHelper))
}else{
putFragment(
ClearentCardOnFileFormFragment.newInstance(cardNavHelper))
}
}
}
And this is my test that fails because Espresso doesn't wait for the lambda function that i pass as parameter to return a response after check the database (it is an inMemoryDatabase)
#Test
fun testFFClearentNativeFormEnable(){
mockWebServer.setDispatcher(BusinessWithAddonsAndPaymentProcessorClearentDispatcher())
val application = ApplicationProvider.getApplicationContext<SchedulicityApplication>()
runBlocking(Dispatchers.Main) {
application.featureFlagsHelper.featureFlagsRepository.featureFlagsDao.insertFeatureFlags(
featureFlagsEnable
)
}
intent.putExtra(Constants.INTENT_KEY_CARD_ON_FILE_EXTRAS, cardNavHelperFromClientRecord)
activityTestRule.launchActivity(intent)
runBlocking { delay(10000) }
populateClearentForm(validCardNumber = false, validExpDate = true)
}
I have to put this delay runBlocking { delay(10000) } other ways fails.
So my question is. Do you know how i can wait for the coroutine response in the UI thread so my test could pass ?
Have a look at the espresso idling resources.