Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
like image 719
JJaviMS Avatar asked Nov 20 '19 08:11

JJaviMS


2 Answers

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.

like image 68
azizbekian Avatar answered Oct 27 '22 00:10

azizbekian


You are missing setting the NavController:

testFragmentScenario.onFragment {
            Navigation.setViewNavController(it.requireView(), mockNavController)
        }
like image 38
coroutineDispatcher Avatar answered Oct 27 '22 01:10

coroutineDispatcher