gpt4 book ai didi

Android:带有协程的易碎 ViewModel 单元测试

转载 作者:行者123 更新时间:2023-12-02 12:52:38 26 4
gpt4 key购买 nike

我有一个虚拟机,例如

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

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

谢谢!

最佳答案

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

有几个不同的选项:

  • CityListViewModel , 你可以避免使用 Dispatchers.IO明确地,而只是依赖 viewModelScope默认为 Dispatchers.Main .在你的真实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将立即返回一个值。
  • 注入(inject) 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是为了确保您的挂起函数将工作定向到另一个调度程序,如果他们正在做阻塞工作。

    关于Android:带有协程的易碎 ViewModel 单元测试,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59563027/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com