Pragmatic UI testing in Jetpack Compose
Use semantic modifiers to automate UI testing in our composables
Introduction — Why automated testing?
As software developers, we should avoid repeating ourselves in non-deterministic and unrepeatable manual tests. Manual tests are slow, unreliable, boring, and of course, cannot run in our CI pipeline :)
With the arrival of Jetpack Compose a lot has changed in how we build our apps, we no longer have views and xml layouts, and alongside composable functions we need to write our instrumented tests differently.
The Old way — Espresso view matching
The basic idea of Espresso library was to find views in our view tree, performing actions and assertions on views that had specific ids.
The New way — Jetpack Compose Testing API
With Jetpack Compose we no longer have views and view matchers, we rely on Composable functions and we have to use a new Testing Api.
// Test rules and transitive dependencies:
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
// Needed for createAndroidComposeRule, but not createComposeRule:
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
Terminology — Rules, Finders, Assertions, Actions and Matchers
The testing API brings a new full set of ways to interact with composable elements:
Rules: set up the necessary environment for a Compose UI test. There are two different rules with different purposes:
- ComposeTestRule → Sets up a UI test without any specific android activity. Factory method: createComposeRule()
- AndroidComposeTestRule → Sets up a UI test within a specific android activity. Factory method: createAndroidComposeRule<YourActivity>(). It may be that in your test you need to access to Android context, in this case, you should use this factory method.
Finders: Useful to select one or multiple nodes in our composables tree, can be used on a single node or a group of nodes. The most common are onNodeWithText, onNodeWithContentDescription
Assertions: Allow us to check that our composable tree contains the expected elements with a predictable behavior. If the condition(s) is verified the test passes, otherwise it fails. Most common are assertExists, assertIsDisplayed, assertTextEquals
Actions: Used to perform a user action on a UI component and change the state of the UI. Most common are performClick(), performScrollTo(), performTextInput()
Matchers: Find nodes that meet certain criteria, can be hierarchical or selectors. The most common are hasParent, hasAnySibling, hasTestTag
Semantics modifier: a lifesaver for testing and accessibility
UI tests in Compose need a way to identify our “views” within the UI hierarchy. As anticipated, we no longer have view IDs, so we need a way to identify uniquely a “piece of ui”. Compose Testing API introduces a testTag modifier for this purpose, however testTag doesn’t add any accessibility info to our UI component.
At the same time, alongside our UI hierarchy, Compose generates a “semantic tree” that describes how our UI is built and gives users important information about the meaning of a UI element, especially if we are using our app with Talkback.
MyButton(
modifier = Modifier.semantics { contentDescription = "Add to favorites" }
)
Semantics modifier helps us to describe the purpose of a UI element and allows us to identify it distinctively.
Wrap it up, test a simple On/Off switch app
Let’s pretend now we have a two-status UI with two buttons, like we had a simple electronic switch.
As you can see from the code, each piece of the UI has its semantics modifier, which improves app accessibility (each node is semantically described) and gives us a way to identify a specific piece of the UI in our instrumented test.
We are now ready to test our UI in an instrumented test :)
Setup the test environment
First of all, we need to import the required dependencies, let’s just add these two lines in our gradle.kts
// UI Tests
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
Then we need our test class, since it’s an instrumented test we should create the class inside androidTest folder.
We need our composeTestRule here, let’s use createAndroidComposeRule builder for the purpose.
class SwitchLayoutKtTest {
// Create test rule, we need android context so we use createAndroidComposeRule factory method
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun testButtonOn_setsOnState() {
// Our test goes here
}
}
Test “ON” Status
When the user clicks on “ON” button, our UI must respond accordingly and status text must contain “Status is: ON”
Test “OFF” Status
Likewise, when the user clicks on “OFF” button, our UI must respond accordingly and status text must contain “Status is: OFF”
Now we can run our tests, hurrah we got a double green!!