ViewModel StateFlow单元测试 – 等待60000毫秒后,测试协程未完成。

huangapple go评论76阅读模式
英文:

ViewModel StateFlow Unit test - After waiting for 60000 ms, the test coroutine is not completing

问题

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.

它在行为方面表现得非常好。

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.

我为ViewModel编写了一个成功和失败的单元测试,但是我遇到了协程超时错误,看起来好像有什么东西从协程中泄漏出来。我不是完全确定。任何帮助将不胜感激。

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

我还检查了一些来自StackOverflow的答案,但没有帮助。

我还打算重写涵盖Loading、Success和Failure场景的ViewModel的测试用例。

ERROR

在等待60000毫秒后测试协程未完成
kotlinx.coroutines.test.UncompletedCoroutinesError: 在等待60000毫秒后测试协程未完成
    在 kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$3$3.invokeSuspend(TestBuilders.kt:342)
    (协程边界)
    在 kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTestCoroutine(TestBuilders.kt:326)
    在 kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invokeSuspend(TestBuilders.kt:167)
    在 kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult$1.invokeSuspend(TestBuildersJvm.kt:13)
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: 在等待60000毫秒后测试协程未完成
    在 kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$3$3.invokeSuspend(TestBuilders.kt:342)
    在 app//kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    在 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())
}
英文:

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&lt;Resource&lt;List&lt;Case&gt;&gt;?&gt;(null)
    val uiCaseListState: StateFlow&lt;Resource&lt;List&lt;Case&gt;&gt;?&gt; get() = _uiCaseListState

    fun fetchCaseList() {

        viewModelScope.launch(Dispatchers.IO) {

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

                    !response.isSuccessful -&gt; {
                        _uiCaseListState.emit(Resource.Failure(ErrorCode.GENERAL_ERROR))
                    }

                    else -&gt; {
                        _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&lt;List&lt;Case&gt;?&gt; {
                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)
    }

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

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

        // Act
        vm.fetchCaseList()

        // Assert
        assertEquals(Resource.Failure&lt;Error&gt;(ErrorCode.GENERAL_ERROR), vm.uiCaseListState.drop(1).first())
    }

答案1

得分: 1

代码中的问题是在测试时没有办法替换调度程序。如果它们在您无法控制的后台线程上运行,就很难在正确的时间执行断言或等待任务完成。

解决方案是通过构造函数在您的 viewModel 中注入调度程序:

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

在测试中,您可以注入一个 TestDispatcher 来替换 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)
}

更多关于测试中使用测试调度程序的信息,请参考这里

英文:

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 -&gt;
                when {
                    response.isSuccessful -&gt; {
                        response.body()?.let { caseList -&gt;
                            val filterCaseList = caseList.sortedByDescending { case -&gt;
                                case.createdDate.time
                            }
                            _uiCaseListState.emit(Resource.Success(filterCaseList))
                        }
                    }

                    !response.isSuccessful -&gt; {
                        _uiCaseListState.emit(Resource.Failure(ErrorCode.GENERAL_ERROR))
                    }

                    else -&gt; {
                        _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&lt;List&lt;Case&gt;?&gt; {
                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)
    }

huangapple
  • 本文由 发表于 2023年6月15日 03:46:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/76477063.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定