I am writing a test for a fragment that uses the Navigation API. Ideally I would like to test the fragment in isolation with something like this:
@RunWith(AndroidJUnit4::class)
class TestDetailFragment {
private lateinit var dao: MyDao
private lateinit var db: MyDatabase
private lateinit var instrumentation: Instrumentation
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
instrumentation = InstrumentationRegistry.getInstrumentation()
db = Room.inMemoryDatabaseBuilder(
context, MyDatabase::class.java).build()
dao = db.getDao()
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
@Test
fun testCreatePosition() {
val input = "testing input"
val entity = MyEntity(1, input)
launchFragmentInContainer<DetailsFragment>()
onView(withId(R.id.text_input))
.perform(
typeText(input),
closeSoftKeyboard()
)
onView(withId(R.id.save)).perform(click())
instrumentation.runOnMainSync {
val liveEntities = dao.getEntities()
liveEntities .observeForever { entities->
assertThat(entities.get(0)).isEqualTo(entity)
}
}
}
}
The problem is that the onClick
listener for my save button uses the app's nav controller to return to the master list view:
class DetailsFragment: Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.details, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<FloatingActionButton>(R.id.save).setOnClickListener {
val db = activity?.let { MyDatabase.getDatabase(it) }!!
val dao = db.getDao()
val input = view.findViewById<EditText>(R.id.text_input).text.toString()
val entity = MyEntity(null, input)
db.queryExecutor.execute{
dao.insert(entity )
}
findNavController().navigate(R.id.list)
}
}
}
Now when I run my test, I get the following error:
java.lang.IllegalStateException: View androidx.constraintlayout.widget.ConstraintLayout{42158d7 V.E...... ........ 0,0-1440,2562} does not have a NavController set
This makes sense since my test is loading the fragment in isolation and not using the activity from my app nor the navigation graph. After a little digging, I found TestNavHostController
and modified my test as follows:
@RunWith(AndroidJUnit4::class)
class TestDetailFragment {
private lateinit var dao: MyDao
private lateinit var db: MyDatabase
private lateinit var instrumentation: Instrumentation
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
instrumentation = InstrumentationRegistry.getInstrumentation()
db = Room.inMemoryDatabaseBuilder(
context, MyDatabase::class.java).build()
dao = db.getDao()
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
@Test
fun testCreatePosition() {
val input = "testing input"
val entity = MyEntity(1, input)
val detailsScenario = launchFragmentInContainer<DetailsFragment>()
val navController = TestNavHostController(
ApplicationProvider.getApplicationContext()
)
detailsScenario.onFragment { fragment ->
navController.setGraph(R.navigation.nav_graph)
Navigation.setViewNavController(fragment.requireView(), navController)
}
onView(withId(R.id.text_input))
.perform(
typeText(input),
closeSoftKeyboard()
)
onView(withId(R.id.save)).perform(click())
instrumentation.runOnMainSync {
val liveEntities = dao.getEntities()
liveEntities .observeForever { entities->
assertThat(entities.get(0)).isEqualTo(entity)
}
}
}
}
This solves the error about a missing NavController
, but now I get
java.lang.IllegalArgumentException: Navigation action/destination com.example:id/list cannot be found from the current destination NavDestination(com.example:id/ListFragment) label=List Fragment
How do I proceed from here? Do I mock out the NavDestination
for my test? This seems like a lot of effort for a test that is intended to assert that the object was created in the database. I plan to test navigation separately.
I realize I'm probably deep down the Y path of my XY problem here. Ultimately, my question is how do I test my fragment when the onClick handler requires a NavController
. Is my approach along the right lines? Or is there a better way? If not, what do I do instead? Is there a good way to modify the onClick handler to not require the NavController
directly? Or to inject it somehow?
The error message says you're on the listFragment
destination, which is not the destination with your list
action.
TestNavHostController
has a second method setCurrentDestination()
for precisely this reason as explained in the Testing Navigation guide:
TestNavHostController provides a setCurrentDestination method that allows you to set the current destination (and optionally, arguments for that destination) so that the NavController is in the correct state before your test begins.
detailsScenario.onFragment { fragment ->
navController.setGraph(R.navigation.nav_graph)
// Use whatever destination ID is associated with your DeatilFragment
navController.setCurrentDestination(R.id.detail)
Navigation.setViewNavController(fragment.requireView(), navController)
}