Fragments serve as reusable containers within your app, allowing you to present the same user interface layout in a variety of activities and layout configurations. Given the versatility of these fragments, it's important to validate that they provide a consistent and resource-efficient experience:
- Your fragment's appearance should be consistent across layout configurations, including those that support larger screen sizes or the landscape device orientation.
- Don't create a fragment's view hierarchy unless the fragment is visible to the user.
This document describes how to include framework-provided APIs in the tests that evaluate each fragment's behavior.
Drive a fragment's state
To help set up the conditions for performing these tests, AndroidX provides a
library, FragmentScenario
, to create fragments and change their state.
Declaring dependencies
In order to use FragmentScenario
as intended, define the fragment-testing
artifact in your app's testing APK, as shown in the following code snippet:
app/build.gradle
dependencies { def fragment_version = "1.1.0" // ... debugImplementation 'androidx.fragment:fragment-testing:$fragment_version' }
To view the current versions for this library, see the information about Fragment on the versions page.
Create a fragment
FragmentScenario
includes methods for launching the following types of
fragments:
The methods also support the following types of fragments:
- Graphical fragments, which contain a user interface. To launch this type of
fragment, call
launchFragmentInContainer()
.FragmentScenario
attaches the fragment to an activity's root view controller. This containing activity is otherwise empty. - Non-graphical fragments (sometimes referred to as headless fragments),
which store or perform short-term processing on information included in several
activities. To launch this type of fragment, call
launchFragment()
.FragmentScenario
attaches this type of fragment to an entirely empty activity, one that doesn't have a root view.
After launching one of these fragment types, FragmentScenario
drives the
fragment under test to the RESUMED
state. This state indicates that the
fragment is running. If you're testing a graphical fragment, it's also visible
to users, so you can evaluate information about its UI elements using Espresso
UI tests.
The following code snippets show how to launch each type of fragment:
Graphical fragment example
@RunWith(AndroidJUnit4::class) class MyTestSuite { @Test fun testEventFragment() { // The "fragmentArgs" and "factory" arguments are optional. val fragmentArgs = Bundle().apply { putInt("selectedListItem", 0) } val factory = MyFragmentFactory() val scenario = launchFragmentInContainer<MyFragment>( fragmentArgs, factory) onView(withId(R.id.text)).check(matches(withText("Hello World!"))) } }
Non-graphical fragment example
@RunWith(AndroidJUnit4::class) class MyTestSuite { @Test fun testEventFragment() { // The "fragmentArgs" and "factory" arguments are optional. val fragmentArgs = Bundle().apply { putInt("numElements", 0) } val factory = MyFragmentFactory() val scenario = launchFragment<MyFragment>(fragmentArgs, factory) } }
Recreate the fragment
If a device is low on resources, the system might destroy the activity
containing your fragment, requiring your app to recreate the fragment when the
user returns to your app. To simulate this situation, call recreate()
:
@RunWith(AndroidJUnit4::class) class MyTestSuite { @Test fun testEventFragment() { val scenario = launchFragmentInContainer<MyFragment>() scenario.recreate() } }
When the FragmentScenario
class recreates the fragment under test, the
fragment returns to the lifecycle state that it was in before being recreated.
Drive the fragment to a new state
In your app's UI tests, it's usually sufficient to just launch and recreate the fragment under test. In finer-grained unit tests, however, you might also evaluate the fragment's behavior as it transitions from one lifecycle state to another.
To drive the fragment to a different lifecycle state, call moveToState()
.
This methods supports the following states as arguments: CREATED
, STARTED
,
RESUMED
, and DESTROYED
. This action simulates a situation where the activity
containing your fragment changes its state because it's interrupted by another
app or a system action.
An example usage of moveToState()
appears in the following code snippet:
@RunWith(AndroidJUnit4::class) class MyTestSuite { @Test fun testEventFragment() { val scenario = launchFragmentInContainer<MyFragment>() scenario.moveToState(State.CREATED) } }
Trigger actions in the fragment
To trigger actions in your fragment under test, use Espresso view matchers to interact with elements in your view:
@RunWith(AndroidJUnit4::class) class MyTestSuite { @Test fun testEventFragment() { val scenario = launchFragmentInContainer<MyFragment>() onView(withId(R.id.refresh)) .perform(click()) } }
If you need to call a method on the fragment itself, such as responding to a
selection in the options menu, you can do so safely by implementing
FragmentAction
:
@RunWith(AndroidJUnit4::class) class MyTestSuite { @Test fun testEventFragment() { val scenario = launchFragmentInContainer<MyFragment>() scenario.onFragment(fragment -> fragment.onOptionsItemSelected(clickedItem) { // Update fragment's state based on selected item. } } } }
Test dialog actions
FragmentScenario
also supports testing of dialogs.
Even though dialogs are instances of graphical fragments, you use the
launchFragment()
method so that the dialog's elements are populated in the
dialog itself, rather than in the activity that launches the dialog.
The following code snippet tests the dialog dismissal process:
@RunWith(AndroidJUnit4::class) class MyTestSuite { @Test fun testDismissDialogFragment() { // Assumes that "MyDialogFragment" extends the DialogFragment class. with(launchFragment<MyDialogFragment>()) { onFragment { fragment -> assertThat(fragment.dialog).isNotNull() assertThat(fragment.requireDialog().isShowing).isTrue() fragment.dismiss() fragment.requireFragmentManager().executePendingTransactions() assertThat(fragment.dialog).isNull() } // Assumes that the dialog had a button // containing the text "Cancel". onView(withText("Cancel")).check(doesNotExist()) } } }