Search code examples
androidandroid-espressoandroid-testingandroid-architecture-componentsandroid-viewmodel

Testing Navigation component: "does not have a NavController"


I'm implementing Espresso tests. I'm using a Fragment with a NavGraph scoped ViewModel. The problem is when I try to test the Fragment I got an IllegalStateException because the Fragment does not have a NavController set. How can I fix this problem?

class MyFragment : Fragment(), Injectable {

    private val viewModel by navGraphViewModels<MyViewModel>(R.id.scoped_graph){
        viewModelFactory
   }

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory
    //Other stuff
}

Test class:

class FragmentTest {

    class TestMyFragment: MyFragment(){
        val navMock = mock<NavController>()

        override fun getNavController(): NavController {
            return navMock
        }
    }

    @Mock
    private lateinit var viewModel: MyViewModel
    private lateinit var scenario: FragmentScenario<TestMyFragment>

    @Before
    fun prepareTest(){
        MockitoAnnotations.initMocks(this)

    scenario = launchFragmentInContainer<TestMyFragment>(themeResId = R.style.Theme_AppCompat){
        TestMyFragment().apply {
            viewModelFactory = ViewModelUtil.createFor(viewModel)
        }
    }

    // My test
}

Exception I got:

java.lang.IllegalStateException: View android.widget.ScrollView does not have a NavController setjava.lang.IllegalStateException

Solution

  • As can be seen in docs, here's the suggested approach:

    // Create a mock NavController
    val mockNavController = mock(NavController::class.java)
    
    scenario = launchFragmentInContainer<TestMyFragment>(themeResId = R.style.Theme_AppCompat) {
        TestMyFragment().also { fragment ->     
            // In addition to returning a new instance of our Fragment,
            // get a callback whenever the fragment’s view is created
            // or destroyed so that we can set the mock NavController
            fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
                if (viewLifecycleOwner != null) {
                    // The fragment’s view has just been created
                    Navigation.setViewNavController(fragment.requireView(), mockNavController)
                }
            }
        }
    }
    

    Thereafter you can perform verification on mocked mockNavController as such:

    verify(mockNavController).navigate(SearchFragmentDirections.showRepo("foo", "bar"))
    

    See architecture components sample for reference.


    There exists another approach which is mentioned in docs as well:

        // Create a graphical FragmentScenario for the TitleScreen
        val titleScenario = launchFragmentInContainer<TitleScreen>()
    
        // Set the NavController property on the fragment
        titleScenario.onFragment { fragment ->
            Navigation.setViewNavController(fragment.requireView(), mockNavController)
        }
    

    This approach won't work in case there happens an interaction with NavController up until onViewCreated() (included). Using this approach onFragment() would set mock NavController too late in the lifecycle, causing the findNavController() call to fail. As a unified approach which will work for all cases I'd suggest using first approach.