Android:使用协程进行片状 ViewModel 单元测试

nol*_*man 5 android kotlin android-testing android-viewmodel kotlin-coroutines

我有一个虚拟机,例如

class CityListViewModel(private val repository: Repository) : ViewModel() {
    @VisibleForTesting
    val allCities: LiveData<Resource<List<City>>> =
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(Resource.Loading())
            emit(repository.getCities())
        }
}
Run Code Online (Sandbox Code Playgroud)

我的测试是:

@ExperimentalCoroutinesApi
class CityListViewModelTest {
    @get:Rule
    val rule = InstantTaskExecutorRule()
    @get:Rule
    val coroutineTestRule = CoroutinesTestRule()

    @Test
    fun `allCities should emit first loading and then a Resource#Success value`() =
        runBlockingTest {
            val fakeSuccessResource = Resource.Success(
                listOf(
                    City(
                        1,
                        "UK",
                        "London",
                        Coordinates(34.5, 56.2)
                    )
                )
            )
            val observer: Observer<Resource<List<City>>> = mock()
            val repositoryMock: Repository = mock()

            val sut =
                CityListViewModel(repositoryMock)
            doAnswer { fakeSuccessResource }.whenever(repositoryMock).getCities()

            sut.allCities.observeForever(observer)
            sut.allCities
            val captor = argumentCaptor<Resource<List<City>>>()
            captor.run {
                verify(observer, times(2)).onChanged(capture())
                assertEquals(fakeSuccessResource.data, lastValue.data)
            }
        }

    @Test
    fun `allCities should emit first loading and then a Resource#Error value`() =
        runBlockingTest {
            val fakeErrorResource = Resource.Error<List<City>>("Error")
            val observer: Observer<Resource<List<City>>> = mock()
            val repositoryMock: Repository = mock()

            val sut =
                CityListViewModel(repositoryMock)
            doAnswer { fakeErrorResource }.whenever(repositoryMock).getCities()

            sut.allCities.observeForever(observer)
            sut.allCities
            val captor = argumentCaptor<Resource<List<City>>>()
            captor.run {
                verify(observer, times(2)).onChanged(capture())
                assertEquals(fakeErrorResource.data, lastValue.data)
            }
        }
}
Run Code Online (Sandbox Code Playgroud)

我遇到的问题是测试非常不稳定:有时它们都通过,有时一个失败,但我似乎无法找出问题所在。

谢谢!

kco*_*ock 7

问题是在测试中,您无法控制 IO 调度程序。我假设你的GistCoroutinesTestRule是这样的?这只是覆盖,但你使用。Dispatchers.MainCityListViewModelDispatchers.IO

有几种不同的选择:

  1. 在 中CityListViewModel,您可以避免Dispatchers.IO显式使用,而只需依赖viewModelScope默认为 的Dispatchers.Main。在您的实际Repository实现中,请确保您的挂起getCities()方法重定向到Dispatchers.IO,即
suspend fun getCities(): List<City> {
  withContext(Dispatchers.IO) {
    // do work
    return cities
  }
}
Run Code Online (Sandbox Code Playgroud)

并在CityListViewModel

  val allCities: LiveData<Resource<List<City>>> = 
    liveData(context = viewModelScope.coroutineContext) {
      emit(Resource.Loading())
      emit(repository.getCities())
    }
Run Code Online (Sandbox Code Playgroud)

在这种情况下,事情将继续像当前一样工作,并且在您的测试中,模拟Repository将立即返回一个值。

  1. 改为注射Dispatchers.IO。如果您使用 Dagger 等 DI 框架,这会更容易,但您基本上可以执行以下操作:
class CityListViewModel(
  private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
  private val repository: Repository
) : ViewModel() {
  @VisibleForTesting
  val allCities: LiveData<Resource<List<City>>> = 
    liveData(context = viewModelScope.coroutineContext + ioDispatcher) {
      emit(Resource.Loading())
      emit(repository.getCities())
    }
}
Run Code Online (Sandbox Code Playgroud)

然后在你的测试中:

val viewModel = CityListViewModel(
  ioDispatcher = TestCoroutineDispatcher(),
  repository = repository
)
Run Code Online (Sandbox Code Playgroud)

其中任何一个都应该使您的测试具有确定性。如果您使用 Dagger,那么我建议同时执行这两种操作(创建一个生产模块来提供 Main、IO 和默认调度程序,但有一个提供实例的测试模块TestCoroutineDispatcher),而且还执行选项 1,即确保您的挂起功能将工作定向到另一个调度程序(如果他们正在执行阻塞工作)。