【问题标题】:Android: flaky ViewModel unit tests with coroutinesAndroid:带有协程的易碎 ViewModel 单元测试
【发布时间】:2020-01-02 12:00:39
【问题描述】:

我有一个虚拟机,例如

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

我的测试是:

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

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

谢谢!

【问题讨论】:

    标签: android kotlin android-testing kotlin-coroutines android-viewmodel


    【解决方案1】:

    问题在于,在测试中,您无法控制 IO Dispatcher。我假设您的CoroutinesTestRule 类似于Gist?这只会覆盖Dispatchers.Main,但您的CityListViewModel 使用Dispatchers.IO。

    有几个不同的选项:

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

    而在CityListViewModel:

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

    在这种情况下,事情将继续像现在一样工作,并且在您的测试中,模拟 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())
        }
    }
    

    然后在你的测试中:

    val viewModel = CityListViewModel(
      ioDispatcher = TestCoroutineDispatcher(),
      repository = repository
    )
    

    其中任何一个都应该使您的测试具有确定性。如果您使用 Dagger,那么我建议您同时使用(创建一个生产模块以提供 Main、IO 和 Default 调度程序,但有一个提供 TestCoroutineDispatcher 实例的测试模块),但也执行选项 1是确保您的挂起函数在执行阻塞工作时将工作定向到另一个调度程序。

    【讨论】:

    • 非常感谢,这是一个很好的答案:原因得到了很好的解释,甚至提供了解决方案。你拯救了我的一天!
    【解决方案2】:

    我相信最好的方法是将 Dispatcher 传递给您的 viewModel,这样测试就超级简单了。就像这里: https://stackoverflow.com/a/62629410/2719243

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-12-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-04
      相关资源
      最近更新 更多