英文:
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的答案,但没有帮助。
- https://stackoverflow.com/questions/72506432/getting-kotlin-error-after-waiting-for-60000-ms-the-test-coroutine-is-not-comp
- https://stackoverflow.com/questions/76052388/asserting-a-coroutine-is-not-complete-under-unit-test
我还打算重写涵盖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
- https://stackoverflow.com/questions/72506432/getting-kotlin-error-after-waiting-for-60000-ms-the-test-coroutine-is-not-comp
- https://stackoverflow.com/questions/76052388/asserting-a-coroutine-is-not-complete-under-unit-test
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())
}
答案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 ->
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)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论