使用MockK测试Kotlin异步函数时遭遇IndexOutOfBoundsException
问题描述
测试Kotlin异步函数时遇到IndexOutOfBoundsException,预期usersInRangeViewStates包含两个元素(第一个为Resource.Loading,第二个为Resource.Success)。尽管已经使用runTest{ ... }和advanceUntilIdle()等待异步执行,但断言有时仍无法等到异步函数完成。
业务代码
val usersInRange: MutableLiveData<Resource<GETUsers>> = MutableLiveData() fun getUsersInRange(myUserGeoPoint: GeoPoint, searchRadiusInMeter: Int) = viewModelScope.launch { usersInRange.postValue(Resource.Loading()) val response = usersInRange(myUserGeoPoint, searchRadiusInMeter) usersInRange.postValue(Resource.Success(response)) } private suspend fun usersInRange(myUserGeoPoint: GeoPoint, searchRadiusInMeter: Int) = withContext(Dispatchers.IO) { val usersInRangeBuffer = GETUsers() if(users.value is Resource.Success) { for(user in (users.value as Resource.Success<GETUsers>).data.users) { if(user.value.id == myUserID) continue if(user.value.isInRange(myUserGeoPoint, searchRadiusInMeter)) { usersInRangeBuffer.users[user.key] = user.value } } } usersInRangeBuffer }
测试代码
private lateinit var usersInRangeViewStates: MutableList<Resource<GETUsers>> @Before fun setUp() { usersInRangeViewStates = mutableListOf() viewModel.usersInRange.observeForever { usersInRangeViewStates.add(it) } } @OptIn(ExperimentalCoroutinesApi::class) @Test fun `getUsersInRange() called should return 0 Users from 2 tested Users (plus myUser)`() = runTest { val myUserGeoPoint = GeoPoint(1.0, 1.0) val searchRadiusInMeter = 1000 val user01ID = "exampleUser01ID" val user02ID = "exampleUser02ID" val user01Location = GETLocation(0.0, 0.0) val user02Location = GETLocation(0.0, 0.0) val myUser = GETUser() val user01 = GETUser(location = user01Location) val user02 = GETUser(location = user02Location) val mapUsers = mutableMapOf(myUserID to myUser, user01ID to user01, user02ID to user02) val users = GETUsers(mapUsers) val getUsers = Resource.Success(users) viewModel.users.value = getUsers viewModel.getUsersInRange(myUserGeoPoint, searchRadiusInMeter) advanceUntilIdle() Assert.assertTrue(usersInRangeViewStates[0] is Resource.Loading) Assert.assertEquals(0, (usersInRangeViewStates[1] as Resource.Success).data.users.size) Assert.assertFalse((usersInRangeViewStates[1] as Resource.Success).data.users.containsKey(user01ID)) Assert.assertFalse((usersInRangeViewStates[1] as Resource.Success).data.users.containsKey(user02ID)) Assert.assertFalse((usersInRangeViewStates[1] as Resource.Success).data.users.containsKey(myUserID)) }
问题根源与解决办法
1. 替换postValue为setValue
postValue会将更新提交到主线程消息队列,即使协程执行完毕,LiveData的回调可能仍未触发。而viewModelScope.launch默认运行在主线程,直接使用setValue可以同步更新LiveData,避免异步延迟:
fun getUsersInRange(myUserGeoPoint: GeoPoint, searchRadiusInMeter: Int) = viewModelScope.launch { usersInRange.setValue(Resource.Loading()) val response = usersInRange(myUserGeoPoint, searchRadiusInMeter) usersInRange.setValue(Resource.Success(response)) }
2. 添加InstantTaskExecutorRule
这个JUnit规则会让LiveData的所有操作同步执行,彻底解决异步回调的时序问题:
@get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
需添加依赖:
testImplementation "androidx.arch.core:core-testing:2.2.0"
3. 控制ViewModel的协程调度器
将ViewModel的协程调度器替换为测试专用的TestDispatcher,确保advanceUntilIdle()能等待所有任务完成:
@OptIn(ExperimentalCoroutinesApi::class) @Before fun setUp() { val testDispatcher = UnconfinedTestDispatcher(testScheduler) Dispatchers.setMain(testDispatcher) // 替换ViewModel的viewModelScope viewModel = YourViewModel().apply { viewModelScope = CoroutineScope(testDispatcher) } usersInRangeViewStates = mutableListOf() viewModel.usersInRange.observeForever { usersInRangeViewStates.add(it) } } @After fun tearDown() { Dispatchers.resetMain() }
4. 先校验列表大小再断言
在访问列表元素前,先确认usersInRangeViewStates的大小符合预期,避免IndexOutOfBoundsException:
Assert.assertEquals(2, usersInRangeViewStates.size) Assert.assertTrue(usersInRangeViewStates[0] is Resource.Loading) // 后续断言逻辑...
内容的提问来源于stack exchange,提问作者Kleini
相关产品推荐
相关产品推荐

