You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 08:27:05