Pragmatic UI testing in Jetpack Compose

Use semantic modifiers to automate UI testing in our composables

Nicola Gallazzi
4 min readDec 29, 2023

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.

A simple ON/OFF switch UI
A simple enum to describe UI status
Our simple Switch ON/OFF Layout

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.

Semantic description of a UI element

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!!

--

--

Nicola Gallazzi

Android Developer with a true passion for clean code. In love with Kotlin. Former Udacity mentor and code reviewer