如何在androidTest上正确模拟ViewModel

时间:2018-04-14 16:16:30

标签: android kotlin mockito android-espresso android-architecture-components

我目前正在为片段编写一些UI单元测试,其中一个@Test是查看对象列表是否正确显示,这是不是集成测试,因此我希望模拟 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,但当然,ViewModel内创建的Fragment不一样。

所以我的问题是,有人成功完成了这项工作,或者有一些指针/参考资料可以帮助我解决这个问题吗?

5 个答案:

答案 0 :(得分:4)

在您的测试设置中,您需要提供正在片段中注入的FavoritesViewModelFactory的测试版本。

您可以执行以下操作,其中需要将模块添加到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)
}

答案 1 :(得分:3)

看起来,您使用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()
}

}

答案 2 :(得分:2)

我使用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这个自定义工厂,该工厂总是返回模拟而不是真正的viewModel: https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearchTest/src/androidTest/java/it/codingjam/github/ui/repo/SearchFragmentTest.kt

答案 3 :(得分:1)

在您提供的示例中,您使用mockito返回视图模型的特定实例的模拟,而不是针对每个实例。

为了完成这项工作,您必须让您的片段使用您创建的精确视图模型模拟。

这很可能来自商店或存储库,所以你可以把你的模拟器放在那里?这实际上取决于您如何在Fragments逻辑中设置视图模型的获取。

建议: 1)模拟视图模型由或构造的数据源 2)添加一个fragment.setViewModel()并将其标记为仅用于测试。这有点难看,但如果你不想模拟数据源,这很容易。

答案 4 :(得分:0)

只需通过以下方式即可轻松模拟ViewModel和其他没有Dagger的对象:

  1. 创建一个可以重新路由对ViewModelProvider的调用的包装类。下面是包装类的生产版本,它只是将调用传递给作为参数传入的真实ViewModelProvider。

    class VMProviderInterceptorImpl : VMProviderInterceptor { override fun get(viewModelProvider: ViewModelProvider, x: Class<out ViewModel>): ViewModel {
        return viewModelProvider.get(x)
    }
    

    }

  2. 将此包装器对象的getter和setter添加到Application类。

  3. 在Activity规则中,在启动一个活动之前,用一个模拟的包装器换出真正的包装器,该包装器不会将get ViewModel调用路由到真正的viewModelProvider,而是提供一个模拟对象。

    < / LI>

    我意识到这不像匕首那么强大,但简单是有吸引力的。