Some Notes Regarding Writing UI Tests for Jetpack Compose Layouts
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:
- Lack of enough documentation,
- Finding composables is sometimes cumbersome,
- Passing
true
touseUnmergedTree
argument, - 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:
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 Intent
s.
That’s all I think! Thanks for taking the time to read this article.