如何在androidTest中正确模拟ViewModel

19

我目前正在为一个片段编写一些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

所以我的问题真正在于,是否有人成功地完成了这个操作或者有一些可以帮助我的指针/参考资料?

5个回答

10

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

您可以执行以下操作,其中模块将需要添加到TestAppComponent中:

@Module
object TestFavoritesViewModelModule {

    val viewModelFactory: FavoritesViewModelFactory = mock()

    @JvmStatic
    @Provides
    fun provideFavoritesViewModelFactory(): FavoritesViewModelFactory {
        return viewModelFactory
    }
}

然后,在测试中,您将能够提供您的模拟视图模型。

fun setupViewModelFactory() {
    whenever(TestFavoritesViewModelModule.viewModelFactory.create(FavoritesViewModel::class.java)).thenReturn(viewModel)
}

嗨,克里斯,这些是非常好的指针,因为我能够在Android P上进行一些测试,但我不确定我是否完全理解了你的解释,你能否再详细说明一下?感谢你。 - Joaquim Ley
你似乎已经成功地模拟了视图模型,查看 Github 项目 - 在非 P 设备上出现了什么错误? 从代码来看,当点击请求按钮时调用的 whenRequestButtonIsClickedViewModelRequestIsCalled 测试当前被忽略且失败,这似乎是由于 RecyclerView 操作引起的。如果使用以下代码替换点击代码,则应该会起作用。onView(withRecyclerView(R.id.recycler_view).atPositionOnView(0, R.id.eta_button)) .check(matches(withText(R.string.action_send_sms))) .perform(click()); - Chris
嗨,克里斯特,我在使用Final类时遇到了一个错误,如果你不在P上运行,就无法模拟ViewModel(Android框架)。| - Joaquim Ley
嘿,@JoaquimLey,你解决了吗? - schwertfisch
有点像,但基本上就是这里所说的。你应该专注于拥有一个模拟工厂,这样你就可以返回ViewModel实例,并且Activity将(间接)获得相同的实例。但是Google已经发布了许多与UI测试相关的新东西,我建议你研究一下这些(Robolectric 4.0和Project Nitrogen)。 - Joaquim Ley

6
我已经使用Dagger注入了一个额外的对象来解决这个问题,你可以在这里找到完整的示例:https://github.com/fabioCollini/ArchitectureComponentsDemo 在Fragment中,我没有直接使用ViewModelFactory,而是定义了一个自定义工厂作为Dagger单例:https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearch/src/main/java/it/codingjam/github/ui/search/SearchFragment.kt 然后在测试中,我使用DaggerMock替换了这个自定义工厂,使用一个总是返回模拟ViewModel而不是真实ViewModel的工厂:https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearchTest/src/androidTest/java/it/codingjam/github/ui/repo/SearchFragmentTest.kt

如果您已经设置了依赖注入,那么这是一个很好的方法。您将使用模拟替换正常数据源,它将运行良好。 - Sam Edwards
非常好的技巧,我将尝试达到相同的效果,但我并不想包含另一个库。非常感谢,我将尝试使用简单的Dagger2实现相同的事情,但仍然无法做到。 :( - Joaquim Ley

4

看起来你正在使用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()
}

}


4
我并没有使用Koin,而且你似乎只是从另一个项目中复制粘贴了随意的代码。最好在这个特定的情况下提供帮助和/或删除不必要/无关的代码。但还是谢谢你的建议! :) - Joaquim Ley

2
在您提供的示例中,您使用mockito为特定实例的视图模型返回模拟,并不是每个实例都返回模拟。
为了使此功能正常工作,您需要确保片段使用您创建的确切视图模型模拟。
最有可能的情况是来自存储或存储库,因此您可以将模拟放在那里?这实际上取决于您如何设置Fragments逻辑中视图模型的获取方式。
建议: 1)模拟构建视图模型的数据源或 2)添加fragment.setViewModel()并将其标记为仅供测试使用。 这有点丑陋,但如果您不想模拟数据源,则以这种方式非常容易。

1
感谢 Sam 的出色贡献,我正在模拟 ViewModel 以获取结果,因为我想保持 Fragment 的 ViewModel 实例私有,但我不知道该如何做到,我会尝试扩展 Fabio 的答案并在 DI 级别上进行改进,非常感谢您的时间 :) - Joaquim Ley

0

一个可以很容易地在没有Dagger的情况下模拟ViewModel和其他对象的方法是:

  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而提供模拟对象的模拟包装器替换真正的包装器。

我意识到这不像Dagger那样强大,但它的简单性很有吸引力。


你能否提供一个更通用的资源链接来解释你的方法?我在想是否可以直接为VM添加包装器。 - superus8r

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接