Some Notes Regarding Writing UI Tests for Jetpack Compose Layouts

Masoud Fallahpour
5 min readSep 10, 2021
Image by Girl with red hat on Unsplash

Jetpack Compose (from now on Compose) is Android’s modern toolkit for building native UIs declaratively. It simplifies and accelerates UI development.

Compose is stable now and thanks to that, a lot of companies and developers are going to use it, if not using it already!

When using Compose, sooner or later, you’ll come to the point when you start writing UI tests to verify the behavior of your layouts.

What follows are my thoughts and findings regarding writing UI tests for Compose layouts.

For the rest of this article, I assume that you know the basics of writing UI tests for Compose layouts.

As an experiment and for getting my feet wet with Compose, I started to migrate the UI of ReleaseTracker from the (classic!) View system to Compose. After implementing the UI of the app, I started to write UI tests, and here are my notes regarding writing UI tests for Compose layouts:

  1. Lack of enough documentation,
  2. Finding composables is sometimes cumbersome,
  3. Passing true to useUnmergedTree argument,
  4. You still need Espresso.

In the following sections, I will elaborate on the aforementioned notes.

Lack of Enough Documentation

Official documentation of Compose is extensive with a pathway, lots of codelabs, and reference documentation. When it comes to testing there are three resources:

  1. Testing
  2. Testing cheat sheet
  3. Testing codelab

Although the above documentations and the codelab can help you get started with testing they quickly fall short when you start to write UI tests for a real-world app.

In order to comprehend the shortage of documentation, you can compare it with the volume of documentation available for Espresso. Espresso docs cover almost everything you need to write UI tests.

When writing UI tests for Compose layouts, I often found myself searching on the Internet to see how to get things done.

Finding Composables Is Sometimes Cumbersome

In the View system, each view can have an ID, and using that, you can easily find them in your UI tests. In Compose, composables have no such thing and that makes finding them hard, if not impossible.

As an example, look at the following code:

Column {
Text(text = "Hello Compose!")
Text(text = "Hello Compose!")
}

In the above code, there is no way to find the first Text composable in a test; in order to perform an action on it or assert something about it.

There are two ways to circumvent this problem:

  • Test tags,
  • Combining matchers.

Test Tags

Think of a test tag as an ID! The name is different but can serve the same purpose: finding composables easily.

To set a test tag for a composable we can use the semantics modifier.

composeRule.onNodeWithTag("text1").assertTextEquals("Hello Compose!")

Using test tags is easy but it has the following drawbacks:

  • It pollutes the production code. By that, I mean these tags have no use other than enabling you to test your UI.
  • There is no help from the IDE for detecting duplicate test tags. If you use the same ID for two views in an XML layout, then Android Studio raises an error. There is no such support for Compose and you should double-check all test tags, in the composable under test, to be unique.

Combining Matchers

The testing libraries provided by Compose have different matchers for finding composables. Sometimes a single matcher is not enough to find a composable. Instead, a combination of them should be used to find the desired composable.

To demonstrate how we can combine matchers to find a composable consider the following piece of code.

The above code displays a list of three countries. Previewed in Android Studio, it looks like this:

Now suppose that we want to write a UI test for ListOfCountries composable and assert that the checkbox next to “Iran” is checked.

Using test tags here does not help. If we assign a test tag (for instance “CountryCheckbox”) to the Checkbox composable then we will have three checkboxes with the same test tag and cannot distinguish them in tests. We will combine matchers to find the desired composable.

In order to find the desired Checkbox composable we should:

  • Find the Text composable with text “Iran”. Call this composable X,
  • From all the sibling composables of X find the composable which is toggleable.

The following compound matcher does it for us:

Now by using the above matcher we can assert that the checkbox next to “Iran” is checked (or is on).

composeRule.onNode(isToggleableWithSiblingText("Iran"))
.assertIsOn()

Passing ‘true’ to ‘useUnmergedTree’ Argument

When building layouts in Compose, the composables form a tree data structure. In addition to this tree, there is another tree, the semantics tree. The semantics tree describes your UI in such a way that it is understandable for accessibility services and for the testing framework. The testing framework uses this tree to find nodes, interact with them, and make assertions.

For several tests that I was writing, they kept failing with the following error although everything seemed correct.

java.lang.AssertionError: Failed to assert the following: ...
Reason: Expected exactly '1' node but could not find any node that satisfies: ...

The above error means that a composable that I wanted to assert about it could not be found. After digging a bit, I found that passing true to useUnmergedTree argument made them all pass. So instead of the following line

composeRule.onNodeWithText("Hello").assertIsDisplayed()

I used

composeRule.onNodeWithText("Hello", useUnmergedTree = true)
.assertIsDisplayed()

The reason is that, by default, finders like onNode, onNodeWithText, etc. use the merged semantics tree. In a merged semantics tree some nodes may merge with each other forming a single node. This may cause some composables not to be found.

You Still Need Espresso

Although Espresso is primarily for testing the UIs implemented with the View system, that does not mean you don’t need it when testing Compose layouts.

As an example, I wanted to dismiss a dialog in one of my tests. The testing artifacts of Compose did not provide a means to do so. After a bit of struggling, I found that using Espresso.pressBack() does the trick.

As another example, I wanted to test if the correct Intent is sent when a particular action is performed. Once again Espresso came to the rescue with its powerful validation and stubbing API for Intents.

That’s all I think! Thanks for taking the time to read this article.

--

--

Masoud Fallahpour

Software engineering @ Klarna. Software engineer, *nix lover, curious learner, gym guy, casual gamer.