How to correctly mock ViewModel on androidTest

22,927

Solution 1

In the example you provided, you are using mockito to return a mock for a specific instance of your view model, and not for every instance.

In order to make this work, you will have to have your fragment use the exact view model mock that you have created.

Most likely this would come from a store or a repository, so you could put your mock there? It really depends on how you setup the acquisition of the view model in your Fragments logic.

Recommendations: 1) Mock the data sources the view model is constructed from or 2) add a fragment.setViewModel() and Mark it as only for use in tests. This is a little ugly, but if you don't want to mock data sources, it is pretty easy this way.

Solution 2

Within your test setup you'll need to provide a test version of the FavoritesViewModelFactory which is being injected in the Fragment.

You could do something like the following, where the Module will need to be added to your TestAppComponent:

@Module
object TestFavoritesViewModelModule {

    val viewModelFactory: FavoritesViewModelFactory = mock()

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

You'd then be able to provide your Mock viewModel in the test.

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

Solution 3

I have solved this problem using an extra object injected by Dagger, you can find the full example here: https://github.com/fabioCollini/ArchitectureComponentsDemo

In the fragment I am not using directly the ViewModelFactory, I have defined a custom factory defined as a Dagger singleton: https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearch/src/main/java/it/codingjam/github/ui/search/SearchFragment.kt

Then in the test I replace using DaggerMock this custom factory using a factory that always returns a mock instead of the real viewModel: https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearchTest/src/androidTest/java/it/codingjam/github/ui/repo/SearchFragmentTest.kt

Solution 4

Look like, you use kotlin and koin(1.0-beta). It is my decision for mocking

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

}

Share:
22,927
Joaquim Ley
Author by

Joaquim Ley

Updated on August 11, 2020

Comments

  • Joaquim Ley
    Joaquim Ley almost 4 years

    I'm currently writing some UI unit tests for a fragment, and one of these @Test is to see if a list of objects is correctly displayed, this is not an integration test, therefore I wish to mock the ViewModel.

    The fragment's vars:

    class FavoritesFragment : Fragment() {
    
        private lateinit var adapter: FavoritesAdapter
        private lateinit var viewModel: FavoritesViewModel
        @Inject lateinit var viewModelFactory: FavoritesViewModelFactory
    
        (...)
    

    Here's the code:

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

    I was able to mock the ViewModel but of course, that's not the same ViewModel created inside the Fragment.

    So my question really, has someone done this successfully or has some pointers/references that might help me out?

  • Joaquim Ley
    Joaquim Ley about 6 years
    Hey Chris, these were very good pointers since I was able to do some tests, but only on Android P. So I'm not entirely sure if I was able to fully understand your explanation, can you please expand? Appreciate you.
  • Sam Edwards
    Sam Edwards about 6 years
    This is a great way to do it if you have dependency injection set up. You are replacing your normal data sources with mocks and it will work well.
  • Chris
    Chris about 6 years
    You seem to have mocked the viewmodel successfully looking at the github project - what errors are you getting on non P devices? Looking at the code the whenRequestButtonIsClickedViewModelRequestIsCalled test which is currently ignored and failing looks to be due to the recyclerview action. If you replace the click code with the following it should work. onView(withRecyclerView(R.id.recycler_view).atPositionOnView‌​(0, R.id.eta_button)) .check(matches(withText(R.string.action_send_sms))) .perform(click());
  • Joaquim Ley
    Joaquim Ley about 6 years
    Hi there Christ, I get an error with Final classes, you can't mock a ViewModel (android framework) if you're not running on P : |
  • Joaquim Ley
    Joaquim Ley about 6 years
    Thank you Sam for your awesome input, I'm currently mocking the VIewModel there so I can get the results, since I want to keep the Fragment's ViewModel instance private, I honestly don't know how, I'll try to expand on Fabio's answer and improve on the DI level, thank you for your time :)
  • Joaquim Ley
    Joaquim Ley about 6 years
    Very nice tips, I'm going to try to accomplish the same, but I didn't really want to include another library. Thank you very much, I'm going to try and do the same thing with simple Dagger2, still wasn't able to though. :(
  • Joaquim Ley
    Joaquim Ley almost 6 years
    I'm not using Koin, and you clearly just copy pasted random code from another project, it would better to help out in this specific case and/or remove unnecessary/unrelated code), but thanks for your input! :)
  • schwertfisch
    schwertfisch over 5 years
    Hey @JoaquimLey did you figure it out?
  • Joaquim Ley
    Joaquim Ley over 5 years
    Sort of, but it is basically what's being said here. You should focus on having a mock factory so you can return your ViewModel instance, and the Activity will (indirectly) get the same instance. But Google has (been) releasing a lot of new things related to UI-Testing I recommend you looking into those (Robolectric 4.0 and Project Nitrogen).
  • superus8r
    superus8r almost 5 years
    Could you maybe please provide a link to a more general resource explaining your method? I was wondering if I could also add the wrapper directly for the VM.