如何在 androidTest 上正确模拟 ViewModel
How to correctly mock ViewModel on androidTest
我目前正在为片段编写一些 UI 单元测试,其中之一 @Test
是查看对象列表是否正确显示,这是 不是集成测试,因此我希望mock ViewModel
.
片段的变量:
class FavoritesFragment : Fragment() {
private lateinit var adapter: FavoritesAdapter
private lateinit var viewModel: FavoritesViewModel
@Inject lateinit var viewModelFactory: FavoritesViewModelFactory
(...)
代码如下:
@MediumTest
@RunWith(AndroidJUnit4::class)
class FavoritesFragmentTest {
@Rule @JvmField val activityRule = ActivityTestRule(TestFragmentActivity::class.java, true, true)
@Rule @JvmField val instantTaskExecutorRule = InstantTaskExecutorRule()
private val results = MutableLiveData<Resource<List<FavoriteView>>>()
private val viewModel = mock(FavoritesViewModel::class.java)
private lateinit var favoritesFragment: FavoritesFragment
@Before
fun setup() {
favoritesFragment = FavoritesFragment.newInstance()
activityRule.activity.addFragment(favoritesFragment)
`when`(viewModel.getFavourites()).thenReturn(results)
}
(...)
// This is the initial part of the test where I intend to push to the view
@Test
fun whenDataComesInItIsCorrectlyDisplayedOnTheList() {
val resultsList = TestFactoryFavoriteView.generateFavoriteViewList()
results.postValue(Resource.success(resultsList))
(...)
}
我能够模拟 ViewModel
但当然,这与在 Fragment
.
中创建的 ViewModel
不同
所以我的问题真的是,有人成功地做到了这一点,或者有一些 pointers/references 可以帮助我吗?
此外,我尝试查看 google-samples 但没有成功。
供参考,项目可以在这里找到: https://github.com/JoaquimLey/transport-eta/
在您的测试设置中,您需要提供 FavoritesViewModelFactory 的测试版本,它被注入到 Fragment 中。
您可以执行如下操作,其中需要将模块添加到您的 TestAppComponent:
@Module
object TestFavoritesViewModelModule {
val viewModelFactory: FavoritesViewModelFactory = mock()
@JvmStatic
@Provides
fun provideFavoritesViewModelFactory(): FavoritesViewModelFactory {
return viewModelFactory
}
}
然后您就可以在测试中提供您的 Mock viewModel。
fun setupViewModelFactory() {
whenever(TestFavoritesViewModelModule.viewModelFactory.create(FavoritesViewModel::class.java)).thenReturn(viewModel)
}
我已经使用 Dagger 注入的额外对象解决了这个问题,您可以在此处找到完整示例:https://github.com/fabioCollini/ArchitectureComponentsDemo
在我没有直接使用 ViewModelFactory 的片段中,我定义了一个定义为 Dagger 单例的自定义工厂:
https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearch/src/main/java/it/codingjam/github/ui/search/SearchFragment.kt
然后在测试中,我用 DaggerMock 这个自定义工厂替换了使用总是 returns 模拟而不是真实 viewModel 的工厂:
https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearchTest/src/androidTest/java/it/codingjam/github/ui/repo/SearchFragmentTest.kt
在您提供的示例中,您正在使用 mockito return 模拟视图模型的特定实例,而不是每个实例。
为了完成这项工作,您必须让您的片段使用您创建的确切视图模型模拟。
这很可能来自商店或存储库,所以您可以将模拟放在那里?这实际上取决于您如何在片段逻辑中设置视图模型的获取。
建议:
1)模拟构建视图模型的数据源或
2) 添加一个 fragment.setViewModel() 并将其标记为仅用于测试。这有点难看,但如果你不想模拟数据源,这种方式很容易。
无需 Dagger 即可轻松模拟 ViewModel 和其他对象:
创建一个可以将调用重新路由到 ViewModelProvider 的包装器 class。下面是包装器 class 的生产版本,它只是将调用传递给作为参数传入的真实 ViewModelProvider。
class VMProviderInterceptorImpl : VMProviderInterceptor { override fun get(viewModelProvider: ViewModelProvider, x: Class<out ViewModel>): ViewModel {
return viewModelProvider.get(x)
}
}
正在将此包装器对象的 getter 和 setter 添加到应用程序 class。
在 Activity 规则中,在启动 activity 之前,将真实包装器替换为不会将 get ViewModel 调用路由到真实 viewModelProvider 的模拟包装器而是提供一个模拟对象。
我意识到这不如 dagger 强大,但它的简单性很有吸引力。
看起来,你使用的是 kotlin 和 koin(1.0-beta)。
嘲讽是我的决定
@RunWith(AndroidJUnit4::class)
class DashboardFragmentTest : KoinTest {
@Rule
@JvmField
val activityRule = ActivityTestRule(SingleFragmentActivity::class.java, true, true)
@Rule
@JvmField
val executorRule = TaskExecutorWithIdlingResourceRule()
@Rule
@JvmField
val countingAppExecutors = CountingAppExecutorsRule()
private val testFragment = DashboardFragment()
private lateinit var dashboardViewModel: DashboardViewModel
private lateinit var router: Router
private val devicesSuccess = MutableLiveData<List<Device>>()
private val devicesFailure = MutableLiveData<String>()
@Before
fun setUp() {
dashboardViewModel = Mockito.mock(DashboardViewModel::class.java)
Mockito.`when`(dashboardViewModel.devicesSuccess).thenReturn(devicesSuccess)
Mockito.`when`(dashboardViewModel.devicesFailure).thenReturn(devicesFailure)
Mockito.`when`(dashboardViewModel.getDevices()).thenAnswer { _ -> Any() }
router = Mockito.mock(Router::class.java)
Mockito.`when`(router.loginActivity(activityRule.activity)).thenAnswer { _ -> Any() }
StandAloneContext.loadKoinModules(hsApp + hsViewModel + api + listOf(module {
single(override = true) { router }
factory(override = true) { dashboardViewModel } bind ViewModel::class
}))
activityRule.activity.setFragment(testFragment)
EspressoTestUtil.disableProgressBarAnimations(activityRule)
}
@After
fun tearDown() {
activityRule.finishActivity()
StandAloneContext.closeKoin()
}
@Test
fun devicesSuccess(){
val list = listOf(Device(deviceName = "name1Item"), Device(deviceName = "name2"), Device(deviceName = "name3"))
devicesSuccess.postValue(list)
onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name1Item"))))
onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name2"))))
onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name3"))))
}
@Test
fun devicesFailure(){
devicesFailure.postValue("error")
onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
Mockito.verify(router, times(1)).loginActivity(testFragment.activity!!)
}
@Test
fun devicesCall() {
onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
Mockito.verify(dashboardViewModel, Mockito.times(1)).getDevices()
}
}
我目前正在为片段编写一些 UI 单元测试,其中之一 @Test
是查看对象列表是否正确显示,这是 不是集成测试,因此我希望mock ViewModel
.
片段的变量:
class FavoritesFragment : Fragment() {
private lateinit var adapter: FavoritesAdapter
private lateinit var viewModel: FavoritesViewModel
@Inject lateinit var viewModelFactory: FavoritesViewModelFactory
(...)
代码如下:
@MediumTest
@RunWith(AndroidJUnit4::class)
class FavoritesFragmentTest {
@Rule @JvmField val activityRule = ActivityTestRule(TestFragmentActivity::class.java, true, true)
@Rule @JvmField val instantTaskExecutorRule = InstantTaskExecutorRule()
private val results = MutableLiveData<Resource<List<FavoriteView>>>()
private val viewModel = mock(FavoritesViewModel::class.java)
private lateinit var favoritesFragment: FavoritesFragment
@Before
fun setup() {
favoritesFragment = FavoritesFragment.newInstance()
activityRule.activity.addFragment(favoritesFragment)
`when`(viewModel.getFavourites()).thenReturn(results)
}
(...)
// This is the initial part of the test where I intend to push to the view
@Test
fun whenDataComesInItIsCorrectlyDisplayedOnTheList() {
val resultsList = TestFactoryFavoriteView.generateFavoriteViewList()
results.postValue(Resource.success(resultsList))
(...)
}
我能够模拟 ViewModel
但当然,这与在 Fragment
.
ViewModel
不同
所以我的问题真的是,有人成功地做到了这一点,或者有一些 pointers/references 可以帮助我吗?
此外,我尝试查看 google-samples 但没有成功。
供参考,项目可以在这里找到: https://github.com/JoaquimLey/transport-eta/
在您的测试设置中,您需要提供 FavoritesViewModelFactory 的测试版本,它被注入到 Fragment 中。
您可以执行如下操作,其中需要将模块添加到您的 TestAppComponent:
@Module
object TestFavoritesViewModelModule {
val viewModelFactory: FavoritesViewModelFactory = mock()
@JvmStatic
@Provides
fun provideFavoritesViewModelFactory(): FavoritesViewModelFactory {
return viewModelFactory
}
}
然后您就可以在测试中提供您的 Mock viewModel。
fun setupViewModelFactory() {
whenever(TestFavoritesViewModelModule.viewModelFactory.create(FavoritesViewModel::class.java)).thenReturn(viewModel)
}
我已经使用 Dagger 注入的额外对象解决了这个问题,您可以在此处找到完整示例:https://github.com/fabioCollini/ArchitectureComponentsDemo
在我没有直接使用 ViewModelFactory 的片段中,我定义了一个定义为 Dagger 单例的自定义工厂: https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearch/src/main/java/it/codingjam/github/ui/search/SearchFragment.kt
然后在测试中,我用 DaggerMock 这个自定义工厂替换了使用总是 returns 模拟而不是真实 viewModel 的工厂: https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearchTest/src/androidTest/java/it/codingjam/github/ui/repo/SearchFragmentTest.kt
在您提供的示例中,您正在使用 mockito return 模拟视图模型的特定实例,而不是每个实例。
为了完成这项工作,您必须让您的片段使用您创建的确切视图模型模拟。
这很可能来自商店或存储库,所以您可以将模拟放在那里?这实际上取决于您如何在片段逻辑中设置视图模型的获取。
建议: 1)模拟构建视图模型的数据源或 2) 添加一个 fragment.setViewModel() 并将其标记为仅用于测试。这有点难看,但如果你不想模拟数据源,这种方式很容易。
无需 Dagger 即可轻松模拟 ViewModel 和其他对象:
创建一个可以将调用重新路由到 ViewModelProvider 的包装器 class。下面是包装器 class 的生产版本,它只是将调用传递给作为参数传入的真实 ViewModelProvider。
class VMProviderInterceptorImpl : VMProviderInterceptor { override fun get(viewModelProvider: ViewModelProvider, x: Class<out ViewModel>): ViewModel { return viewModelProvider.get(x) }
}
正在将此包装器对象的 getter 和 setter 添加到应用程序 class。
在 Activity 规则中,在启动 activity 之前,将真实包装器替换为不会将 get ViewModel 调用路由到真实 viewModelProvider 的模拟包装器而是提供一个模拟对象。
我意识到这不如 dagger 强大,但它的简单性很有吸引力。
看起来,你使用的是 kotlin 和 koin(1.0-beta)。 嘲讽是我的决定
@RunWith(AndroidJUnit4::class)
class DashboardFragmentTest : KoinTest {
@Rule
@JvmField
val activityRule = ActivityTestRule(SingleFragmentActivity::class.java, true, true)
@Rule
@JvmField
val executorRule = TaskExecutorWithIdlingResourceRule()
@Rule
@JvmField
val countingAppExecutors = CountingAppExecutorsRule()
private val testFragment = DashboardFragment()
private lateinit var dashboardViewModel: DashboardViewModel
private lateinit var router: Router
private val devicesSuccess = MutableLiveData<List<Device>>()
private val devicesFailure = MutableLiveData<String>()
@Before
fun setUp() {
dashboardViewModel = Mockito.mock(DashboardViewModel::class.java)
Mockito.`when`(dashboardViewModel.devicesSuccess).thenReturn(devicesSuccess)
Mockito.`when`(dashboardViewModel.devicesFailure).thenReturn(devicesFailure)
Mockito.`when`(dashboardViewModel.getDevices()).thenAnswer { _ -> Any() }
router = Mockito.mock(Router::class.java)
Mockito.`when`(router.loginActivity(activityRule.activity)).thenAnswer { _ -> Any() }
StandAloneContext.loadKoinModules(hsApp + hsViewModel + api + listOf(module {
single(override = true) { router }
factory(override = true) { dashboardViewModel } bind ViewModel::class
}))
activityRule.activity.setFragment(testFragment)
EspressoTestUtil.disableProgressBarAnimations(activityRule)
}
@After
fun tearDown() {
activityRule.finishActivity()
StandAloneContext.closeKoin()
}
@Test
fun devicesSuccess(){
val list = listOf(Device(deviceName = "name1Item"), Device(deviceName = "name2"), Device(deviceName = "name3"))
devicesSuccess.postValue(list)
onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name1Item"))))
onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name2"))))
onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name3"))))
}
@Test
fun devicesFailure(){
devicesFailure.postValue("error")
onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
Mockito.verify(router, times(1)).loginActivity(testFragment.activity!!)
}
@Test
fun devicesCall() {
onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
Mockito.verify(dashboardViewModel, Mockito.times(1)).getDevices()
}
}