Blog Infos
Author
Published
Topics
, , ,
Published

UI created with XML is traditionally tested with Espresso and UIAutomator. However, Jetpack Compose constructs UI in differently and the usual tools can’t handle some of its specifics.

Composable instead of View. Jetpack Compose constructs UI with Composables and doesn’t use Android Views. Composable is also a UI element. It has semantics that describes its attributes. All composables are combined in a single UI tree with semantics that describes its children.

Compose Layout doesn’t have IDs and tags. Instead, there’s the testTag attribute in semantics that allows you to add a unique identifier to a Composable.

Different testing tools. Espresso and UIAutomator can still test a Compose Layout — searching by text, resource, etc. However, they don’t have access to Composables’ semantics and can’t fully test them. Therefore, it’s recommended to use the Jetpack Compose testing library as it can access semantics and fully test Composables on the screen.

Compose tests are synchronized by default. Moreover, they don’t run in real time, but use a virtual clock so they can pass as fast as possible.

Use AndroidComposeTestRule or ComposeTestRule test rule.

Add testing dependencies to the build.gradle file:

def compose_version = '1.0.1'
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

Let’s assume we have a screen with a single button.

The layout for this screen:
@Composable
fun MainScreen() {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(Color.White)
) {
Button(
onClick = {...},
modifier = Modifier.testTag("yourTestTag")
) {
Text(text = stringResource(R.string.click))
}
}
}

Now we should make sure it is displayed on the screen and then click on it. How is this done?

To test the screen we first need to open a test class in the androidTest folder.
Create a testRule with createAndroidTestRule. Pass the activity class that holds the UI:

class ExampleInstrumentedTest {
@get:Rule
val composeTestRule = createAndroidTestRule(MainActivity::class.java)

Now write a test where the button is found by its testTag. Check that it is displayed and then click on it.

@Test
fun testButtonClick() {
val button = composeTestRule.onNode(hasTestTag("yourTestTag"), useUnmergedTree = true)
button.assertIsDisplayed()
button.performClick()
}

In this test we have:
composeTestRule – a TestRule to test UI created with Compose
onNode – a finder
hasTestTag – a Matcher
useUnmergedTree – a parameter that controls UI tree hierarchy representation
asssertExists – an assertion
performClick – an action

composeTestRule finds the UI element by its semantics attributes such as testTag, content description, or a custom property of a Composable. It has access the entire semantics tree of the UI that is on a screen.

Finders look for the Composable with a matching criterion and return a SemanticsNodeInteraction that holds the Composable and its children if there are any.

Some common finders:

  • onNode — looks for a single Composable that matches the searching criteria. Throws an exception if more than one matching Composable is found.
  • onAllNodes — looks for all nodes with a matching criterion. Returns a non-iterable SemanticsNodeInteractionCollection that holds found Composables and its possible children.
  • onNodeWithTag — looks for a single Composable with the specified testTag
  • onNodeWithText — looks for a single Composable with the specified text. A localized string can be searched by retrieving it with
androidComposeTestRule.activity.getString(R.string.*)

A matcher specifies the criteria a finder uses to find the Composable. For example:

  • hasContentDescription — verifies that the Composable has specified content description.
  • hasTestTag — verifies that the Composable has the specified test tag.
  • isRoot — verifies that it is the root Composable.

There are also hierarchical matchers and selectors.

Hierarchical matchers verify the position of the Composable in the UI tree with methods like hasParent() or hasAnyChild().

Selectors can figure out Composables around and filter them.
For example, given the following tree:

|-Root composable
  |-ButtonOne
  |-ButtonTwo
  |-ButtonThree

calling onSiblings() on ButtonTwo will return buttonOne and buttonThree Composables.

The full list of matchers is below.

Compose layout flattens its UI tree so some UI elements can be combined into a single Composable. For example, 2 texts can be merged into a single Text Composable. Thus, some semantics can be lost. In order to inspect an intact UI tree useUnmergedTree should be true .

They verify that the Composable meets a specific condition.

Some common assertions:

  • assertExists
  • assertIsEnabled
  • assertTextEquals
  • assertContentDescription

Using generic assert(), you can provide your matcher and verify that it is satisfied for this node.

Actions simulate user events on Composable such us:

  • performClick
  • performScroll
  • performTextInput

It also supports different kinds of gestures.

The full list of Finders, Matchers, Assertions, and Actions can be found in Jetpack Compose testing cheatsheet.

Jetpack Compose also allows testing only the layout itself instead of the entire app.
To do this use createComposeRule instead of createAndroidComposeRule.

@get:Rule
val composeTestRule = createComposeRule()

And then set the layout Composable(MainScreen) right in the test:

@Test
fun testButtonClick() {
composeTestRule.setContent {
MyAppTheme {
MainScreen()
}
}
val button = composeTestRule.onNode(hasTestTag("yourTestTag"), true)
button.assertIsDisplayed()
button.performClick()
}

It is even possible to create the UI right inside the test:

@Test
fun testButtonClick() {
composeTestRule.setContent {
Column {
Button(
onClick = {...},
modifier = Modifier.testTag("yourTestTag")
) {
Text(text = "Click")
}
}
}
val button = composeTestRule.onNode(hasTestTag("yourTestTag"), true)
button.assertIsDisplayed()
button.performClick()
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, , ,

The Evolution of Android UI Development. A Journey From XML to Compose. What’s next ?

In this talk, we will take a deep dive into the history of Android UI and witness how it has evolved over the years. We will start from Activity UI Java codes, move to XML,…
Watch Video

The Evolution of Android UI Development. A Journey From XML to Compose. What’s next ?

Daniel Rubzoff & Mert Kilic
Android Engineer
Just Eat Takeaway (JET)

The Evolution of Android UI Development. A Journey From XML to Compose. What’s next ?

Daniel Rubzoff & M ...
Android Engineer
Just Eat Takeaway (J ...

The Evolution of Android UI Development. A Journey From XML to Compose. What’s next ?

Daniel Rubzoff & ...
Android Engineer
Just Eat Takeaway (JET)

Jobs

Is Composable compatible with View?

  • Yes, they are interoperable. It is possible to add an Android View to Composable and vice versa.

Can I use Espresso and UIAutomator to test UI created with Jetpack Compose?

  • Yes. You can search on the UI by text or resource to find the elements and interact with them.

What’s the difference between createComposeRule and createAndroidComposeRule?

  • createAndroidComposeRule is an Android-specific TestRule as it holds a reference to the activity it runs.
    createComposeRule is crossplatform and has no ties to Android.

 

androidx.test.core.app.InstrumentationActivityInvoker

It took me a good deal of time to figure out what was the root cause of the issue. Turned out, UI tests for Compose cannot run properly if the tested activity launchMode is singleInstance

android:launchMode="singleInstance"

Removing this attribute will fix the issue. However, if it’s not an option, then there are a few other ways to fix it:

  1. Override/remove the attribute for UI test with a different manifest

When assembling an app, Gradle merges manifests that your app, dependencies, and modules may have.
You can override or remove completely the android:launchMode attribute by node markers.
By default, androidTest runs in the debug build type. So adding a proper node marker to Manifest in the/debug directory will override it for the app used by androidTest tests.

⚠️ However, it will also affect ordinary debug builds. Read further if it’s undesirable.

2. Create a separate build variant for Compose UI tests

Just making a separate build type solves the issue and also keeps UI tests available for multiple app flavors.
Don’t forget adding testBuildType “staging” // TODO Update this line

⚠️ Build type can’t be named compose as it is a reserved word.

In my case, the root cause of the issue was due to using the wrong scrolling functionality in LazyList.

I could only fix the issue using animateScrollToItem(index) instead of scrollToItem(index).

Testing with Compose Layout

Android Codelabs for Jetpack Compose Testing

Testing cheatsheet

Android Developers Backstage: Episode 171: Compose Testing

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

1 Comment. Leave new

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu