0

I am working on Android project and making API calls to fetch data from the server. I am using Retrofit for networking and StateFlow for storing and passing data to the UI layer.

It is working perfectly fine, behavior-wise.

I wrote a unit test for ViewModel for Success, Failure, but I am getting a Coroutine timeout error and it looks like something is leaking from Coroutine. I am not completely sure. Any help would be appreciated.

I also checked few answers from StackOverflow and it's not helpful

I am also open to rewrite test cases for viewmodel that covers Loading, Success & Failure scenarios.

ERROR

After waiting for 60000 ms, the test coroutine is not completing
kotlinx.coroutines.test.UncompletedCoroutinesError: After waiting for 60000 ms, the test coroutine is not completing
    at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$3$3.invokeSuspend(TestBuilders.kt:342)
    (Coroutine boundary)
    at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTestCoroutine(TestBuilders.kt:326)
    at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invokeSuspend(TestBuilders.kt:167)
    at kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult$1.invokeSuspend(TestBuildersJvm.kt:13)
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: After waiting for 60000 ms, the test coroutine is not completing
    at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$3$3.invokeSuspend(TestBuilders.kt:342)
    at app//kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)

ViewModel


    private val _uiCaseListState = MutableStateFlow<Resource<List<Case>>?>(null)
    val uiCaseListState: StateFlow<Resource<List<Case>>?> get() = _uiCaseListState

    fun fetchCaseList() {

        viewModelScope.launch(Dispatchers.IO) {

            inboxRepository.fetchCaseList().collect { response ->
                when {
                    response.isSuccessful -> {
                        response.body()?.let { caseList ->
                            val filterCaseList = caseList.sortedByDescending { case ->
                                case.createdDate.time
                            }
                            _uiCaseListState.emit(Resource.Success(filterCaseList))
                        }
                    }

                    !response.isSuccessful -> {
                        _uiCaseListState.emit(Resource.Failure(ErrorCode.GENERAL_ERROR))
                    }

                    else -> {
                        _uiCaseListState.emit(Resource.Failure(ErrorCode.GENERAL_ERROR))
                    }

                }
            }
        }

    }

Unit Test

  @Test
    fun fetchCaseList_Success_CaseList() = runTest {

        // Arrange
        val repo = InboxRepository(object : InboxApi {
            override suspend fun fetchCaseList(): Response<List<Case>?> {
                return Response.success(testCaseList)
            }
        })
        val vm = InboxViewModel(repo)

        // Act
        vm.fetchCaseList()

        // Assert
        assertEquals(Resource.Success(testCaseList).data.size, vm.uiCaseListState.drop(1).first()!!.data!!.size)
    }

    /**
     * "kotlinx.coroutines.test.UncompletedCoroutinesError:After waiting for 60000 ms, the test coroutine is not completing"
     */
    @Ignore
    @Test
    fun fetchCaseList_Failure_GENERAL_ERROR() = runTest {

        // Arrange
        val error: Response<List<Case>?> = Response.error(
            400,
            "{\"key\":[\"someError\"]}"
                .toResponseBody("application/json".toMediaTypeOrNull())
        )
        val repo = InboxRepository(object : InboxApi {
            override suspend fun fetchCaseList(): Response<List<Case>?> {
                return error
            }
        })
        val vm = InboxViewModel(repo)

        // Act
        vm.fetchCaseList()

        // Assert
        assertEquals(Resource.Failure<Error>(ErrorCode.GENERAL_ERROR), vm.uiCaseListState.drop(1).first())
    }
Arpit Patel
  • 7,212
  • 5
  • 56
  • 67
  • I don't know if this is what happens here, but I believe there is a race condition in this code. You launch fetching concurrently, so at the time you do `drop(1)` the flow could contain the failure already. Then it waits forever. If you can't do this sequentially, then instead of dropping the first item, you can filter out null. – broot Jun 15 '23 at 06:16

1 Answers1

1

The problem with your code is that there is no way to replace dispatcher while testing. It can be difficult to perform assertions at the correct time or to wait for tasks to complete if they’re running on background threads that you have no control over.

The solution is to inject dispatcher in your viewModel through constructor :

 InboxViewModel(val repo: InboxRepository,val ioDispatcher: CoroutineDispatcher = Dispatchers.IO)

  viewModelScope.launch(ioDispatcher) {

            inboxRepository.fetchCaseList().collect { response ->
                when {
                    response.isSuccessful -> {
                        response.body()?.let { caseList ->
                            val filterCaseList = caseList.sortedByDescending { case ->
                                case.createdDate.time
                            }
                            _uiCaseListState.emit(Resource.Success(filterCaseList))
                        }
                    }

                    !response.isSuccessful -> {
                        _uiCaseListState.emit(Resource.Failure(ErrorCode.GENERAL_ERROR))
                    }

                    else -> {
                        _uiCaseListState.emit(Resource.Failure(ErrorCode.GENERAL_ERROR))
                    }

                }
            }
        }

    }

In tests, you can inject a TestDispatcher to replace the Dispatchers.IO.

 @Test
    fun fetchCaseList_Success_CaseList() = runTest {

        // Arrange
        val repo = InboxRepository(object : InboxApi {
            override suspend fun fetchCaseList(): Response<List<Case>?> {
                return Response.success(testCaseList)
            }
        })
        val vm = InboxViewModel(repo, Dispatchers.Unconfined)

        // Act
        vm.fetchCaseList()

        // Assert
        assertEquals(Resource.Success(testCaseList).data.size, vm.uiCaseListState.drop(1).first()!!.data!!.size)
    }
Asad Mahmood
  • 532
  • 1
  • 5
  • 15