android / architecture-components-samples

Samples for Android Architecture Components.
https://d.android.com/arch
Apache License 2.0
23.44k stars 8.28k forks source link

Mock ViewModel on Fragment UI Test #196

Closed nuxzero closed 7 years ago

nuxzero commented 7 years ago

From GithubBrowserSample project. I got problem when I mock ViewModel to use to test Fragment but it look like mock not working. It still call Repository in ViewModel.

Here is code in test class

   @Before
    fun setUp() {
        viewModel = mock(AViewModel::class.java)
        `when`(viewModel.results).thenReturn(results)
        fragment = AFragment()
        fragment.viewModelFactory = ViewModelUtil.createFor(viewModel)
        activityRule.activity.setFragment(fragment)
    }

Inside fragment

open class AFragment : Fragment() {
    @Inject lateinit var viewModelFactory: ViewModelProvider.Factory
    private lateinit var viewModel: AViewModel
    lateinit var binding: AFragmentBinding

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.a_fragment, container, false)
        return binding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel = ViewModelProviders.of(this, viewModelFactory).get(AViewModel::class.java)
        ...
    }
...

Do I need to use the FragmentDataBindingComponent from the sample?

nuxzero commented 7 years ago

My solution for this problem. I hope this will help some people that got problem about mock ViewModel for test fragment.

AFragmentTest.kt

@RunWith(AndroidJUnit4::class)
class AFragmentTest {

    @Rule
    @JvmField
    val activityRule = ActivityTestRule(TestSingleFragmentActivity::class.java, true, true)

    @Rule
    @JvmField
    val taskRule = TaskExecutorWithIdlingResourceRule()

    private lateinit var fragment: AFragment
    private lateinit var viewModel: AViewModel
    private val aData = MutableLiveData<Resource<A>>()
    private lateinit var fragmentBindingAdapters: FragmentBindingAdapters
    private lateinit var fragmentDataBindingComponent: FragmentDataBindingComponent

    @Before
    fun setUp() {
        fragment = AFragment()
        viewModel = mock(AViewModel::class.java)
        fragment.viewModel = viewModel

        // Mock DataBindingComponent and set to fragment
        fragmentBindingAdapters = mock(FragmentBindingAdapters::class.java)
        fragmentDataBindingComponent = mock(FragmentDataBindingComponent::class.java)
        `when`(fragmentDataBindingComponent.fragmentBindingAdapters).thenReturn(fragmentBindingAdapters)
        fragment.dataBindingComponent = fragmentDataBindingComponent

        `when`(viewModel.aData).thenReturn(aData)

        activityRule.activity.setFragment(fragment)
    }
...

AFragment.kt

class AFragment : Fragment() {

    @Inject lateinit var viewModelFactory: ViewModelProvider.Factory
    var viewModel: AViewModel? = null
    lateinit var binding: AFragmentBinding

    // DataBindingComponent required for UI test
    var dataBindingComponent: DataBindingComponent = FragmentDataBindingComponent(this)

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Also need to set dataBindingComponent to binding object.
        binding = DataBindingUtil.inflate(inflater, R.layout.a_fragment, container, false, dataBindingComponent)
        return binding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        // This protected fragment create new viewModel after create mock. 
        // If mock already created, no need to set viewModel.
        if(viewModel  == null) {
            viewModel = ViewModelProviders.of(this, viewModelFactory).get(AViewModel::class.java)
        }

    }
...
JoaquimLey commented 6 years ago

This works ofc, but this is not the proper solution I would say, you are exposing your ViewModel, it should be private, and if possible not set after fragment instantiation.

SrishtiRoy commented 5 years ago

i am using roboelectric for the same ,but still could not mock viewmodel

JoaquimLey commented 5 years ago

@SrishtiRoy have you tried mocking the ViewModel factory and return your own viewmodel instance?

Joaquim Ley

🌎: joaquimley.com http://www.joaquimley.com/ 📍Copenhagen, Denmark [image: Twitter Joaquim Ley] https://twitter.com/joaquimley https://github.com/joaquimley https://twitch.tv/joaquimley https://medium.com/@JoaquimLey

On Wed, Jan 30, 2019 at 5:04 AM SrishtiRoy notifications@github.com wrote:

i am using roboelectric for the same ,but still could not mock viewmodel

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/googlesamples/android-architecture-components/issues/196#issuecomment-458804027, or mute the thread https://github.com/notifications/unsubscribe-auth/AGmh_LuvvDitPpQ3dSO8FIXUY6L2WUPnks5vIRm-gaJpZM4PyBd5 .

SrishtiRoy commented 5 years ago

@JoaquimLey that worked ,just want to know does Data Binding in xml does not work with roboelectric, i am not able to access value that are set in viewmodel as observable field [@Test public void clickLogInButton_CallsViewModel() { Assert.assertNotNull(fragment);

    Button fav = fragment.getView().findViewById(R.id.fav);
    assertTrue(fav.getVisibility() == View.VISIBLE);
    assertEquals(fav.getText(),"Unfavorite");

    fav.performClick();

   verify(mockViewModel).setFavorite();
}](url)
JoaquimLey commented 5 years ago

Sorry, can't help you with that, never used robolectric with databinding :\

vejei commented 4 years ago

The reason the mock object does not work is because it was replaced when the dependency was injected,here is my solution to mock the ViewModel, in your test class add:

val viewModel = mock(MyViewModel::class.java)

fragmentManager.registerFragmentLifecycleCallbacks(object : FragmentManager.FragmentLifecycleCallbacks() {
    /*
    In general, dependencies are injected in the onAttach method.
    So, we can replace the injected ViewModel with a mock ViewModel after the method called.
    The method execution order in fragment: onAttach -> onCreate -> onCreateView -> onFragmentViewCreated
    */
    override fun onFragmentViewCreated(
        fm: FragmentManager,
        f: Fragment,
        v: View,
        savedInstanceState: Bundle?
    ) {
        (f as SignInFragment).viewModel = viewModel // replace ViewModel with the mock one
    }
}, false)