Search code examples
kotlinasynchronousjunitkotlin-coroutinesmockk

IndexOutOfBoundsExpection when testing asynchronous functions in Kotlin with mockk


When testing a function I have problems with an IndexOutOfBoundsException. Normally, usersInRangeViewStates should have size two, with the first entry Resource.Loading and the second Resource.Success. Because usersInRange(...) is an asynchronous function and I have to wait for it, I added runTest{ ... } and advanceUntilIdle(). I actually thought that the test with the Asserts then waits until the asynchronous function is finished, but that only happens sometimes. Does anyone see the error or can you suggest me a better solution?

Code:

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
}

Test:

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

Solution

  • viewModel.getUsersInRange sends Resource.Success(response) to the MutableLiveData, which then asks the main thread to run usersInRangeViewStates.add(it).

    advanceUntilIdle will wait for "run observer" task to be scheduled. But because the observing is done outside of coroutines, it probably will not wait for the task to actually finish running.

    I think you can do your testing as another thing to run on the main thread.

    withContext(Dispatchers.Main) {
        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))
    }