1. Before you begin
In previous codelabs you learned about navigation using the Navigation Component. In this codelab, you'll learn how to test Navigation Component. Please note that this is different from testing navigation without the use of a navigation component.
Prerequisites
- You have created test directories in Android Studio.
- You have written unit and instrumentation tests in Android Studio.
- You have added Gradle dependencies to an Android project.
What you'll learn
- How to use instrumentation tests to test navigation component.
- How to set up tests without repeated code.
What you need
- A computer with Android Studio installed.
- The solution code for the Words app.
Download the starter code for this codelab
In this codelab you will add instrumentation tests to solution code for the Words app.
- Navigate to the provided GitHub repository page for the project.
- Verify that the branch name matches the branch name specified in the codelab. For example, in the following screenshot the branch name is main.
- On the GitHub page for the project, click the Code button, which brings up a popup.
- In the popup, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
- Locate the file on your computer (likely in the Downloads folder).
- Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.
Open the project in Android Studio
- Start Android Studio.
- In the Welcome to Android Studio window, click Open.
Note: If Android Studio is already open, instead, select the File > Open menu option.
- In the file browser, navigate to where the unzipped project folder is located (likely in your Downloads folder).
- Double-click on that project folder.
- Wait for Android Studio to open the project.
- Click the Run button
to build and run the app. Make sure it builds as expected.
2. Starter app overview
The Words app consists of a home screen that shows a list, with each list item being a letter of the alphabet. Clicking a letter navigates to a screen showing a list of words that start with that letter.
3. Create the tests directories
If necessary, create an instrumentation test directory for the Words app as you have done in previous codelabs. If you have already done this, then you can skip ahead to Add the necessary dependencies.
4. Create an instrumentation test Class
Create a new class called NavigationTests.kt in the androidTest folder.
5. Add the necessary dependencies
Testing navigation component requires some specific Gradle dependencies. We will also include a dependency that lets us test fragments in a very specific way. Navigate to your app module's build.gradle file and add the following dependency:
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.4.0'
androidTestImplementation 'androidx.navigation:navigation-testing:2.5.2'
debugImplementation 'androidx.fragment:fragment-testing:1.5.3'
Now sync your project.
6. Write the Navigation Component Test
Testing Navigation Component differs from testing regular navigation. When we test regular navigation, we trigger the navigation to execute on the device or emulator. When we test Navigation Component, we don't actually make the device or emulator visibly navigate. Instead, we force the navigation controller to navigate without actually changing what is seen on the device or emulator, and then we check to make sure that the navigation controller arrived at the correct destination.
- Create a test function called
navigate_to_words_nav_component()
. - Working with Navigation Component in tests requires some setup. In the
navigate_to_words_nav_component()
method, create a test instance of the navigation controller.
val navController = TestNavHostController(
ApplicationProvider.getApplicationContext()
)
- Navigation Component drives the UI using Fragments. There is a fragment equivalent of
ActivityScenarioRule
that can be used to isolate a fragment for testing, which is why the fragment-specific dependency is required. This can be very useful for testing a fragment that requires a lot of navigation to reach, because it can instead be launched without any additional code to handle navigating to it.
val letterListScenario = launchFragmentInContainer<LetterListFragment>(themeResId =
R.style.Theme_Words)
Here we specify that we want to launch the LetterListFragment
. We have to pass the app's theme so that the UI component knows which theme to use or the test may crash.
- Lastly, we need to explicitly declare which navigation graph we want the nav controller to use for the fragment launched.
letterListScenario.onFragment { fragment ->
navController.setGraph(R.navigation.nav_graph)
Navigation.setViewNavController(fragment.requireView(), navController)
}
- Now trigger the event that prompts the navigation.
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions
.actionOnItemAtPosition<RecyclerView.ViewHolder>(2, click()))
When using the launchFragmentInContainer()
method, actual navigation is not possible because the container is not aware of other fragments or activities that we might be navigating to. It only knows the fragment that we specified to launch in it. Therefore, when you run this test on a device or emulator, you will not see any actual navigation. This may seem unintuitive, but it allows us to make a much more direct assertion regarding the current destination. Instead of looking for a UI component that we know displays on a particular screen, we can check to make sure that the current navigation controller's destination has the ID of the fragment we expect to be in. This approach is significantly more reliable than the aforementioned.
assertEquals(navController.currentDestination?.id, R.id.wordListFragment)
Your test should look something like this:
7. Solution code
8. Avoid repeat code with annotations
In Android, both instrumentation tests and unit tests have a feature that lets us set up the same configuration for every test in a class without repeating code.
Say, for example, that we had a fragment with 10 buttons. Each button leads to a unique fragment when clicked.
If we followed the pattern in the test above, we would have to repeat code that looked like this for each of the 10 tests (note that this code is strictly an example and will not compile in the app we worked with in this codelab):
val navController = TestNavHostController(
ApplicationProvider.getApplicationContext()
)
val exampleFragmentScenario = launchFragmentInContainer<ExampleFragment>(themeResId =
R.style.Theme_Example)
exampleFragmentScenario.onFragment { fragment ->
navController.setGraph(R.navigation.example_nav_graph)
Navigation.setViewNavController(fragment.requireView(), navController)
}
That's a lot of code to repeat 10 times. In this case, however, we can save ourselves some time by using the @Before
annotation provided byJUnit. We use this by annotating a method where we then provide the code needed to set up our test. We can name the method whatever we like, but it should be relevant. Instead of setting up the same fragment 10 times, we can write the setup code once like this:
lateinit var navController: TestNavHostController
lateinit var exampleFragmentScenario: FragmentScenario<ExampleFragment>
@Before
fun setup(){
navController = TestNavHostController(
ApplicationProvider.getApplicationContext()
)
exampleFragmentScenario = launchFragmentInContainer(themeResId=R.style.Theme_Example)
exampleFragmentScenario.onFragment { fragment ->
navController.setGraph(R.navigation.example_nav_graph)
Navigation.setViewNavController(fragment.requireView(), navController)
}
}
This method now runs for every test that we write in this class, and we can access the necessary variables from any one of the tests.
Similarly, if there is code that we need to execute after every test, we can use the @After
annotation. For example, @After
can be used to clean up a resource that we used for our test, or for instrumentation tests, we can use it to return the device to a particular state.
JUnit also provides the @BeforeClass
and @AfterClass
annotations. The difference here is that methods with this annotation execute once, but the executed code still applies to every method. If your setup or teardown methods contain expensive operations, it may be preferable to use these annotations instead. Methods annotated with @BeforeClass
and @AfterClass
must be placed in a companion object and annotated with @JvmStatic
. To demonstrate the order of execution of these annotations, let's take a look at the following code:
Remember, @BeforeClass
runs for the class, @Before
runs before the functions, @After
runs after the functions, and @AfterClass
runs for the class. Can you predict what the output of this will look like?
The order of execution for the functions is setupClass()
, setupFunction()
, test_a()
, tearDownFunction()
, setupFunction()
, test_b()
, tearDownFunction()
, setupFunction()
, test_c()
, tearDownFunction()
, tearDownClass()
. This makes sense because @Before
and @After
run before and after every method, respectively. @BeforeClass
runs once before anything in the class runs and @AfterClass
runs once after everything else in the class has run.
9. Congratulations
In this codelab you:
- Learned how to test Navigation Component.
- Learned how to avoid repetitive code with the
@Before
,@BeforeClass
,@After
, and@AfterClass
annotations.