How to test Jetpack Compose
Tags: android, jetpackcompose, testing, kotlin
Published: 2021-09-16 08:14:13


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.
<br />
## Jetpack Compose vs XML
**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](https://mvnrepository.com/artifact/androidx.ui/ui-test?repo=google) 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**.
<br /><br />
## Getting started

Add testing dependencies to the `build.gradle` file:

```groovy
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.
<div style="text-align:center"><img src="https://miro.medium.com/max/1400/1*Ca5UOIt-SZOcPWPROzlR7w.png" alt="button screen" /></div>

The layout for this screen:

```kotlin
@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?
<br /><br />
## Testing the screen

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:

```kotlin
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.

```kotlin
@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
<br /><br />
## Now each step in detail

### composeTestRule
composeTestRule finds the UI element by its semantics attributes such as testTag, content description, or a [custom property](https://developer.android.com/jetpack/compose/testing#custom-semantics-properties) of a Composable. It has access the entire semantics tree of the UI that is on a screen.

### Finders

Finders look for the Composable with a matching criterion and return a [SemanticsNodeInteraction](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/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](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/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 
```kotlin 
androidComposeTestRule.activity.getString(R.string.*)
```


### Matchers
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.

### useUnmergedTree

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`. 

### Assertions
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
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](https://developer.android.com/images/jetpack/compose/compose-testing-cheatsheet.png).<br /><br />
## Testing only layout

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

```kotlin
@get:Rule
val composeTestRule = createComposeRule()
```

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

```kotlin
        @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:

```kotlin
    @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()
    }
```
<br /><br />
## Q&A

Is Composable compatible with View?

- Yes, they are [interoperable](https://developer.android.com/jetpack/compose/interop/interop-apis). 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.
<br /><br />
## Troubleshooting

### Blank screen when testing Jetpack Compose UI
<div style="text-align:center"><img width="50%" height="50%" src="error.png" alt="androidx.test.core.app.InstrumentationActivityInvoker" /></div>

```kotlin
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
```kotlin
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](https://developer.android.com/studio/build/manifest-merge) manifests that your app, dependencies, and modules may have.
   You can override or remove completely the `android:launchMode` attribute by [node markers](https://developer.android.com/studio/build/manifest-merge#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](https://developer.android.com/studio/build/build-variants)** 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.


### lateinit property remeasurement has not been initialized in LazyList

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).

<br /><br />
## Further reads

[Testing with Compose Layout](https://developer.android.com/jetpack/compose/testing)

[Android Codelabs for Jetpack Compose Testing](https://developer.android.com/codelabs/jetpack-compose-testing#0)

[Testing cheatsheet](https://developer.android.com/jetpack/compose/testing-cheatsheet)

[Android Developers Backstage: Episode 171: Compose Testing](http://adbackstage.libsyn.com/episode-171-compose-testing)