Kotlin Flow unit testing exceptions and job with FlowOn with different Dispatchers - android

Using the code below to test exceptions with Flow and MockK
#Test
fun given network error returned from repos, should throw exception() =
testCoroutineScope.runBlockingTest {
// GIVEN
every { postRemoteRepository.getPostFlow() } returns flow<List<PostDTO>> {
emit(throw Exception("Network Exception"))
}
// WHEN
var expected: Exception? = null
useCase.getPostFlow()
.catch { throwable: Throwable ->
expected = throwable as Exception
println("⏰ Expected: $expected")
}
.launchIn(testCoroutineScope)
// THEN
println("⏰ TEST THEN")
Truth.assertThat(expected).isNotNull()
Truth.assertThat(expected).isInstanceOf(Exception::class.java)
Truth.assertThat(expected?.message).isEqualTo("Network Exception")
}
And prints
⏰ TEST THEN
⏰ Expected: java.lang.Exception: Network Exception
And the test fails with
expected not to be: null
Method i test is
fun getPostFlow(): Flow<List<Post>> {
return postRemoteRepository.getPostFlow()
// 🔥 This method is just to show flowOn below changes current thread
.map {
// Runs in IO Thread DefaultDispatcher-worker-2
println("⏰ PostsUseCase map() FIRST thread: ${Thread.currentThread().name}")
it
}
.flowOn(Dispatchers.IO)
.map {
// Runs in Default Thread DefaultDispatcher-worker-1
println("⏰ PostsUseCase map() thread: ${Thread.currentThread().name}")
mapper.map(it)
}
// This is a upstream operator, does not leak downstream
.flowOn(Dispatchers.Default)
}
This is not a completely practical function but just to check how Dispatchers work with Flow and tests.
At the time of writing the question i commented out .flowOn(Dispatchers.IO), the one on top
the test passed. Also changing Dispatchers.IO to Dispatchers.Default and caused test to pass. I assume it's due to using different threads.
1- Is there a function or way to set all flowOn methods to same thread without modifying the code?
I tried testing success scenario this time with
#Test
fun `given data returned from api, should have data`() = testCoroutineScope.runBlockingTest {
// GIVEN
coEvery { postRemoteRepository.getPostFlow() } returns flow { emit(postDTOs) }
every { dtoToPostMapper.map(postDTOs) } returns postList
val actual = postList
// WHEN
val expected = mutableListOf<Post>()
// useCase.getPostFlow().collect {postList->
// println("⏰ Collect: ${postList.size}")
//
// expected.addAll(postList)
// }
val job = useCase.getPostFlow()
.onEach { postList ->
println("⏰ Collect: ${postList.size}")
expected.addAll(postList)
}
.launchIn(testCoroutineScope)
job.cancel()
// THEN
println("⏰ TEST THEN")
Truth.assertThat(expected).containsExactlyElementsIn(actual)
}
When i tried to snippet that is commented out, test fails with
java.lang.IllegalStateException: This job has not completed yet
It seems that test could pass if job has been canceled, the one with launchIn that returns job passes.
2- Why with collect job is not canceled, and it only happens when the first flowOn uses Dispatchers.IO?

Related

kotlinx.coroutines.test the latest API usage

I am working on a test case for ViewModel classes with the recent coroutines-test API and it doesn't work as expected.
#Test
fun `when balanceOf() is called with existing parameter model state is updated with correct value`() = runTest {
Dispatchers.setMain(StandardTestDispatcher())
fakeWalletRepository.setPositiveBalanceOfResponse()
assertThat("Model balance is not default", subj.uiState.value.wallet.getBalance().toInt() == 0)
assertThat("Errors queue is not empty", subj.uiState.value.errors.isEmpty())
assertThat("State is not default", subj.uiState.value.status == Status.NONE)
subj.balanceOf("0x6f1d841afce211dAead45e6109895c20f8ee92f0")
advanceUntilIdle()
assertThat("Model balance is not updated with correct value", subj.uiState.value.wallet.getBalance().toLong() == 42L)
assertThat("Errors queue is not empty", subj.uiState.value.errors.isEmpty())
assertThat("State is not set as BALANCE", subj.uiState.value.status == Status.BALANCE)
}
he issue is not working stably - usually it fails, under debugger usually it passes.
Based on my understanding StandartTestDispatcher shouldn't run coroutines until advanceUntilIdle call when UnconfinedTestDispatcher run them immediately. advanceUntilIdle should wait until all coroutines are finished, but it seems there is a race condition in the next assertThat() call which causes ambiguity in the behaviour of my test case.
advanceUntilIdle should guarantee all coroutines end their work. Does it mean race condition occurs somewhere under .collect{} or state.update {} calls? (In my understanding advanceUntilIdle should wait end of their execution too)
fun balanceOf(owner: String) {
logger.d("[start] balanceOf()")
viewModelScope.launch {
repository.balanceOf(owner)
.flowOn(Dispatchers.IO)
.collect { value ->
logger.d("collect get balance result")
processBalanceOfResponse(value)
}
}
logger.d("[end] balanceOf()")
}
state.update {
it.wallet.setBalance(value.data)
it.copy(wallet = it.wallet, status = Status.BALANCE)
}
From what i see the balanceOf() is executed on the IO dispatcher and you collect in the viewModelScope (which is the Main.Immediate dispatcher). The IO dispatcher is not overridden in your test and this is what causes the unpredictability of your test.
As there’s currently no way to override the IO dispatcher like with setMain, you can add the ability to override the background dispatcher in your ViewModel by adding a default argument, for example :
ViewModel(private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO)
And replace it in your code :
fun balanceOf(owner: String) {
logger.d("[start] balanceOf()")
viewModelScope.launch {
repository.balanceOf(owner)
.flowOn(backgroundDispatcher)
.collect { value ->
logger.d("collect get balance result")
processBalanceOfResponse(value)
}
}
logger.d("[end] balanceOf()")
}
Then in your test you instantiate the ViewModel with the standard test dispatcher and it should work. You can check this page to understand the issue : https://developer.android.com/kotlin/coroutines/test#injecting-test-dispatchers

Kotlin Coroutines Flow catch mechanism

In my sample I'm calling network operation and emitting success case but on error e.g 404 app crashes wihout emitting exception. Surrendering with try catch prevent crashes but I want to pass error till the ui layer like success case.
suspend fun execute(
params: Params,
):
Flow<Result<Type>> = withContext(Dispatchers.IO) {
flow {
emit(Result.success(run(params)))
}.catch {
emit(Result.failure(it))
}
}
There is a helpful function runCatching for creating a Result easily, but the problem in coroutines is that you don't want to be swallowing CancellationExceptions. So below, I'm using runCatchingCancellable from my answer here.
This shouldn't be a Flow since it returns a single item.
If run is a not a blocking function (it shouldn't be if you are using Retrofit with suspend functions), your code can simply be:
suspend fun execute(params: Params): Result<Type> = runCatchingCancellable {
run(params)
}
If it is a blocking function you can use:
suspend fun execute(params: Params): Result<Type> = runCatchingCancellable {
withContext(Dispatchers.IO) {
run(params)
}
}
If you were going to return a Flow (which you shouldn't for a returning a single item!!), then you shouldn't make this a suspend function, and you should catch the error inside the flow builder lambda:
fun execute(params: Params): Flow<Result<Type>> = flow {
emit(runCatchingCancellable {
run(params)
})
}
// or if run is blocking (it shouldn't be):
fun execute(params: Params): Flow<Result<Type>> = flow {
emit(runCatchingCancellable {
withContext(Dispatchers.IO) { run(params) }
})
}
If you want to use flows you can use the catch method of flows.
As you said you can use try-catch but it would break the structured concurrency since it would catch the cancellation exception as well or it would avoid the cancellation exception to be thrown.
One thing that you can do is to use an Exception handler at the point where you launch the root coroutine that calls the suspend function.
val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
// handle it
}
scope.launch(handler) { // root coroutine
execute(params)
somethingThatShouldBeExecutedOnlyIfPreviousCallDoesNotThrow()
}
This solution is good for both flows and non-flow coroutines.
In the solution with the runCatching you will have to manually check the result of the first execute to avoid the second one to run.
One interesting thread is here.

How to make higher order functions synchronous kotlin?

I am trying to run a test in which I want to wait till higher order function executes. As of now I am not able to figure out any ways to do it. Following is my code.
#Test
fun `test execute routine error`() = runBlocking(coroutineDispatcher) {
val observer = mock<Observer<String>>()
baseViewModel.error.observeForever(observer)
val httpException = HttpException(Response.error<String>(402, mock(ResponseBody::class.java)))
val function = baseViewModel.executeRoutine {
throw httpException
}
verify(observer).onChanged("Something went wrong. Please try again")
}
The problem with above snippet is that it jumps to the last line i.e. verify() before throwing an http exception for executeRoutine.
Update: Execute routine definition
fun executeRoutine(requestType: RequestType = RequestType.POST_LOGIN, execute: suspend () -> Unit) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
_spinner.postValue(true)
try {
execute()
} catch (ex: HttpException) {
val errorHandler = errorHandlerFactory.create(requestType)
_error.postValue(errorHandler.getErrorMessageFrom(ex))
} catch (ex: Exception) {
_error.postValue(ex.localizedMessage)
Timber.e(ex)
} finally {
_spinner.postValue(false)
}
}
}
}
The problem is that the higher order function does execute, it just doesn't do what you think it does -- its execution is launching the task, not waiting for it to complete.
You will have to solve the problem another way, by either having your test wait until the change is observed, or having the callback complete a barrier to allow the test to proceed (e.g. completableJob.complete() at the end of the call back, and completableJob.join() waiting before proceeding with the test).
It might also be desirable to rearchitect your code so you don't have to do anything special, e.g. by making executeRoutine a suspend function executing the code rather than launching the code in another scope.

Coroutine Unit Testing Fails for the Class

I am facing a weird issue while unit testing Coroutines. There are two tests on the class, when run individually, they both pass and when I run the complete test class, one fails with assertion error.
I am using MainCoroutineRule to use the TestCoroutineScope and relying on the latest Coroutine Testing Library
Here is the test :
#Test
fun testHomeIsLoadedWithShowsAndFavorites() {
runBlocking {
// Stubbing network and repository calls
whenever(tvMazeApi.getCurrentSchedule("US", currentDate))
.thenReturn(getFakeEpisodeList())
whenever(favoriteShowsRepository.allFavoriteShowIds())
.thenReturn(arrayListOf(1, 2))
}
mainCoroutineRule.runBlockingTest {
// call home viewmodel
homeViewModel.onScreenCreated()
// Check if loader is shown
assertThat(LiveDataTestUtil.getValue(homeViewModel.getHomeViewState())).isEqualTo(Loading)
// Observe on home view state live data
val homeViewState = LiveDataTestUtil.getValue(homeViewModel.getHomeViewState())
// Check for success data
assertThat(homeViewState is Success).isTrue()
val homeViewData = (homeViewState as Success).homeViewData
assertThat(homeViewData.episodes).isNotEmpty()
// compare the response with fake list
assertThat(homeViewData.episodes).hasSize(getFakeEpisodeList().size)
// compare the data and also order
assertThat(homeViewData.episodes).containsExactlyElementsIn(getFakeEpisodeViewDataList(true)).inOrder()
}
}
The other test is almost similar which tests for Shows without favorites. I am trying to test HomeViewModel method as:
homeViewStateLiveData.value = Loading
val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
onError(exception)
}
viewModelScope.launch(coroutineExceptionHandler) {
// Get shows from network and favorites from room db on background thread
val favoriteShowsWithFavorites = withContext(Dispatchers.IO) {
val favoriteShowIds = favoriteShowsRepository.allFavoriteShowIds()
val episodes = tvMazeApi.getCurrentSchedule(COUNTRY_US, currentDate)
getShowsWithFavorites(episodes, favoriteShowIds)
}
// Return the combined result on main thread
withContext(Dispatchers.Main) {
onSuccess(favoriteShowsWithFavorites)
}
}
}
I cannot find the actual cause of why the tests if run separately are passing and when the complete class is tested, one of them is failing. Pls help if I am missing something
Retrofit and Room that come with Coroutine support owner the suspend functions and move them off the UI thread by their own. Thus, they reduce the hassles of handling thread callbacks by the developers in a big way. Initially, I was moving the suspend calls of network and DB to IO via Dispatchers.IO explicitly. This was unnecessary and also leading unwanted context-switching leading to flaky test. Since the libraries, automatically do it, it was just about handling the data back on UI when available.
viewModelScope.launch(coroutineExceptionHandler) {
// Get favorite shows from db, suspend function in room will launch a new coroutine with IO dispatcher
val favoriteShowIds = favoriteShowsRepository.allFavoriteShowIds()
// Get shows from network, suspend function in retrofit will launch a new coroutine with IO dispatcher
val episodes = tvMazeApi.getCurrentSchedule(COUNTRY_US, currentDate)
// Return the result on main thread via Dispatchers.Main
homeViewStateLiveData.value = Success(HomeViewData(getShowsWithFavorites(episodes, favoriteShowIds)))
}

Kotlin coroutines using produces and mockito to mock the producing job

I am testing Kotlin coroutines in my Android app and I am trying to do the following unit test
#Test fun `When getVenues success calls explore venues net controller and forwards result to listener`() =
runBlocking {
val near = "Barcelona"
val result = buildMockVenues()
val producerJob = produce<List<VenueModel>>(coroutineContext) { result.value }
whenever(venuesRepository.getVenues(eq(near))) doReturn producerJob // produce corooutine called inside interactor.getVenues(..)
interactor.getVenues(near, success, error) // call to real method
verify(venuesRepository).getVenues(eq(near))
verify(success).invoke(argThat {
value == result.value
})
}
The interactor method is as follows
fun getVenues(near: String, success: Callback<GetVenuesResult>,
error: Callback<GetVenuesResult>) =
postExecute {
repository.getVenues(near).consumeEach { venues ->
if (venues.isEmpty()) {
error(GetVenuesResult(venues, Throwable("No venues where found")))
} else {
success(GetVenuesResult(venues))
}
}
}
postExecute{..} is a method on a BaseInteractor that executes the function in the ui thread through a custom Executor that uses the launch(UI) coroutine from kotlin android coroutines library
fun <T> postExecute(uiFun: suspend () -> T) =
executor.ui(uiFun)
Then the repository.getVenues(..) function is also a coroutine that returns the ProducerJob using produce(CommonPool) {}
The problem is that it seams that success callback in the interactor function doesn't seem to be executed as per the
verify(success).invoke(argThat {
value == result.value
})
However, I do see while debugging that the execution in the interactor function reaches to the if (venues.isEmpty()) line inside the consumeEach but then from there exits and continues with the test, obviously failing on the verify for the success callback.
I am a bit new on coroutines so any help would be appreciated.
I figured this one out. I saw that the problem was just with this producing coroutine and not with the others tests that are also using coroutines and working just fine. I noticed that I actually missed the send on the mocked ProducingJob in order to have it actually produce a value, in this case the list of mocks. I just added that changing the mock of the producing job to
val producerJob = produce { send(result.value) }

Categories

Resources