12

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
azizbekian
  • 60,783
  • 13
  • 169
  • 249
JJaviMS
  • 392
  • 6
  • 18

2 Answers2

34

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.

azizbekian
  • 60,783
  • 13
  • 169
  • 249
  • The Illegal exception has been fixed, but now I got a problem with the navMock, when the ViewModel is being created it tries to get the backstack, but as it is a Mock null is returned and the test fails. As it is a Mock I can´t stub the method that gets the NavBackStackEntry, and I can´t find an easy way to test a Fragment with `navGraphViewModels ` – JJaviMS Nov 27 '19 at 08:24
  • Seems like the issue you outline is related to other thing, which actually has no connection to this question. At this point I do not have enough information in order to understand what's the issue with your code. Feel free to outline detailed error log and code so that I can have a glance at it. – azizbekian Nov 27 '19 at 08:45
  • 1
    @azizbekian This approach works but only if you don't need to set a graph on your mocked navcontroller. I'm struggling with this issue, case here: https://stackoverflow.com/questions/60191801/navigation-components-mocking-navcontroller-graph – Nicola Gallazzi Mar 05 '20 at 09:27
  • I get an error which say backstack is empty – TheFedex87 Dec 07 '21 at 11:24
  • you will need to set navController.setViewModelStore(ViewModelStore()) if you pass some data – AITAALI_ABDERRAHMANE Mar 22 '22 at 13:31
  • Neither of these approaches work if findNavController() is called from onCreate(). How do we test navigation from onCreate()? (e.g. a conditional redirect) – Chuck Stein Aug 15 '23 at 22:13
1

You are missing setting the NavController:

testFragmentScenario.onFragment {
            Navigation.setViewNavController(it.requireView(), mockNavController)
        }
coroutineDispatcher
  • 7,718
  • 6
  • 30
  • 58