2021年5月6日星期四

Testing an Android Fragment that uses Navigation

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?

https://stackoverflow.com/questions/67428415/testing-an-android-fragment-that-uses-navigation May 07, 2021 at 10:51AM

没有评论:

发表评论