We’re developing a hybrid Jetpack Compose application. It’s comprised of one Activity
and several Fragments
. Each fragment includes a ComposeView
directly as the entire screen is built with Compose. But how do we write tests for this?
The problem
Using a hybrid Compose architecture reduced the learning curve for the team. However, this meant we had to make a tradeoff. Instead of using the Navigation component with Compose support, we’re using a standard nav_graph.xml
file with destinations defined in XML.
In order to test the various composables housed inside of fragment classes, we’d need to effectively navigate to the desired fragment.
The solution
AndroidComposeTestRule. The AndroidComposeTestRule
was specifically designed for the use-case where compose content is hosted by an Activity. This test rule allows you to gain access to the activity and thus its navController
. Which is exactly what we need to successfully navigate to the various fragments.
Getting set up
We’ll start by pulling in the appropriate dependencies. In my application, I’m using Gradle’s Version Catalogs. Version Catalogs allow you to easily add dependencies with autocompletion support in Android Studio.
First, add the following dependencies to the libs.versions.toml
file:
compose-ui-test-junit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" } espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espresso" }
I’ve included the Espresso Intents
dependency as well. This will allow us to validate the click listeners that start new activities.
Next, in your module’s build.gradle.kts
file you will need to reference those dependencies:
// Espresso intents androidTestImplementation(libs.espresso.intents) // Compose test rules and transitive dependencies androidTestImplementation(libs.compose.ui.test.junit) // Needed for createComposeRule debugImplementation(libs.compose.ui.test.manifest)
👩🏽💻 Don’t forget to add the testInstrumentationRunner
defaultConfig { ... testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }
Due to a configuration issue, I also needed to add the following packaging options to get the tests to build:
packagingOptions { resources.excludes += "META-INF/AL2.0" resources.excludes += "META-INF/LGPL2.1" }
Now we have just what we need to start writing some tests.
Adding test tags
Unlike View-based UIs, you don’t just reference a composable by id. To interact with the UI hierarchy you’ll need to use Semantics. In this context, semantics is how you mark a piece of Compose UI for later access.
The most straightforward approach I found is to create string constants and then add them to the desired composable as a Modifier.testTag()
. Here are a few examples.
const val NON_ORGANIZER_MAIN_IMAGE = "non_organizer_main_image" const val NON_ORGANIZER_MAIN_TEXT = "non_organizer_main_text" const val NON_ORGANIZER_CONTACT_US = "non_organizer_contact_us"
Once the tags are defined, I add them to the composables using the modifier
property.
Image( painter = painterResource(id = R.drawable.bicycle), modifier = Modifier .fillMaxWidth() .testTag(NON_ORGANIZER_MAIN_IMAGE), )
TextButton( onClick = onContactUsClick, modifier = Modifier.testTag(NON_ORGANIZER_CONTACT_US) )
Now we have a way to access these components in our tests.
Writing tests
We’ll start by navigating to our desired fragment, the NonOrganizerFragment
. This is where the AndroidComposeTestRule
comes into play. Before each test, we use the test rule to gain access to the activity’s navController
and then navigate to the fragment.
@get:Rule val composeTestRule = createAndroidComposeRule<MainActivity>() @Before fun goToNonOrganizerFragment() { composeTestRule.activityRule.scenario.onActivity { findNavController(it, R.id.nav_host_fragment) .navigate(R.id.nonOrganizerFragment) } }
Now let’s write our first test. We want to confirm that the static screen elements are displayed. We’ll use our test tags that we defined previously to do that.
composeTestRule.onNodeWithTag(TAG).assertIsDisplayed()
The composeTestRule
has a function onNodeWithTag
that allows you to find a composable with a particular test tag. If the composable is found you can make various assertions. For this simple case, we only want to ensure that the element is displayed.
Putting it all together, I created a list of the test tags I was interested in. Then used the onEach
statement to test that they were displayed.
@Test fun shouldDisplayStaticContent() { listOf( NON_ORGANIZER_MAIN_IMAGE, NON_ORGANIZER_MAIN_TEXT, NON_ORGANIZER_CONTACT_US, NON_ORGANIZER_MEMBER_APP ).onEach { composeTestRule.onNodeWithTag(it) .assertIsDisplayed() } }
You can view the full test class here where I’ve also included an example of using Espresso Intents
.
Resources
Here are some useful resources to help you with your Compose UI testing:
Thanks for reading!
[…] Testing Hybrid Jetpack Compose Apps […]