Testing your Compose layout

Testing UIs or screens is used to verify the correct behavior of your Compose code, improving the quality of your app by catching errors early in the development process.

Compose provides a set of testing APIs to find elements, verify their attributes and perform user actions. They also include advanced features such as time manipulation.

Semantics

UI tests in Compose use semantics to interact with the UI hierarchy. Semantics, as the name implies, give meaning to a piece of UI. In this context, a "piece of UI" (or element) can mean anything from a single composable to a full screen. The semantics tree is generated alongside the UI hierarchy, and describes it.

Diagram showing a typical UI layout, and the way that layout would map to a corresponding semantic tree

Figure 1. A typical UI hierarchy and its semantics tree.

The semantics framework is primarily used for accessibility, so tests take advantage of the information exposed by semantics about the UI hierarchy. Developers decide what and how much to expose.

A button containing a graphic and text

Figure 2. A typical button containing an icon and text.

For example, given a button like this that consists of an icon and a text element, the default semantics tree only contains the text label "Like". This is because some composables, such as Text, already expose some properties to the semantics tree. You can add properties to the semantic tree by using a Modifier.

MyButton(
    modifier = Modifier.semantics { contentDescription = "Add to favorites" }
)

Setup

This section describes how to set up your module to let you test compose code.

First, add the following dependencies to the build.gradle file of the module containing your UI tests:

// 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")

This module includes a ComposeTestRule and an implementation for Android called AndroidComposeTestRule. Through this rule you can set Compose content or access the activity. The rules are constructed using factory functions createComposeRule, or createAndroidComposeRule if you need access to an activity. A typical UI test for Compose looks like this:

// file: app/src/androidTest/java/com/package/MyComposeTest.kt

class MyComposeTest {

    @get:Rule val composeTestRule = createComposeRule()
    // use createAndroidComposeRule<YourActivity>() if you need access to
    // an activity

    @Test
    fun myTest() {
        // Start the app
        composeTestRule.setContent {
            MyAppTheme {
                MainScreen(uiState = fakeUiState, /*...*/)
            }
        }

        composeTestRule.onNodeWithText("Continue").performClick()

        composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
    }
}

Testing APIs

There are three main ways to interact with elements:

  • Finders let you select one or multiple elements (or nodes in the Semantics tree) to make assertions or perform actions on them.
  • Assertions are used to verify that the elements exist or have certain attributes.
  • Actions inject simulated user events on the elements, such as clicks or other gestures.

Some of these APIs accept a SemanticsMatcher to refer to one or more nodes in the semantics tree.

Finders

You can use onNode and onAllNodes to select one or multiple nodes respectively, but you can also use convenience finders for the most common searches, such as onNodeWithText , onNodeWithContentDescription, etc. You can browse the complete list in the Compose Testing cheat sheet.

Select a single node

composeTestRule.onNode(<<SemanticsMatcher>>, useUnmergedTree = false): SemanticsNodeInteraction
// Example
composeTestRule
    .onNode(hasText("Button")) // Equivalent to onNodeWithText("Button")

Select multiple nodes

composeTestRule
    .onAllNodes(<<SemanticsMatcher>>): SemanticsNodeInteractionCollection
// Example
composeTestRule
    .onAllNodes(hasText("Button")) // Equivalent to onAllNodesWithText("Button")

Using the unmerged tree

Some nodes merge the semantics information of their children. For example, a button with two text elements merges their labels:

MyButton {
    Text("Hello")
    Text("World")
}

From a test, we can use printToLog() to show the semantics tree:

composeTestRule.onRoot().printToLog("TAG")

This code prints the following output:

Node #1 at (...)px
 |-Node #2 at (...)px
   Role = 'Button'
   Text = '[Hello, World]'
   Actions = [OnClick, GetTextLayoutResult]
   MergeDescendants = 'true'

If you need to match a node of what would be the unmerged tree, you can set useUnmergedTree to true:

composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG")

This code prints the following output:

Node #1 at (...)px
 |-Node #2 at (...)px
   OnClick = '...'
   MergeDescendants = 'true'
    |-Node #3 at (...)px
    | Text = '[Hello]'
    |-Node #5 at (83.0, 86.0, 191.0, 135.0)px
      Text = '[World]'

The useUnmergedTree parameter is available in all finders. For example, here it's used in an onNodeWithText finder.

composeTestRule
    .onNodeWithText("World", useUnmergedTree = true).assertIsDisplayed()

Assertions

Check assertions by calling assert() on the SemanticsNodeInteraction returned by a finder with one or multiple matchers:

// Single matcher:
composeTestRule
    .onNode(matcher)
    .assert(hasText("Button")) // hasText is a SemanticsMatcher

// Multiple matchers can use and / or
composeTestRule
    .onNode(matcher).assert(hasText("Button") or hasText("Button2"))

You can also use convenience functions for the most common assertions, such as assertExists , assertIsDisplayed , assertTextEquals , etc. You can browse the complete list in the Compose Testing cheat sheet.

There are also functions to check assertions on a collection of nodes:

// Check number of matched nodes
composeTestRule
    .onAllNodesWithContentDescription("Beatle").assertCountEquals(4)
// At least one matches
composeTestRule
    .onAllNodesWithContentDescription("Beatle").assertAny(hasTestTag("Drummer"))
// All of them match
composeTestRule
    .onAllNodesWithContentDescription("Beatle").assertAll(hasClickAction())

Actions

To inject an action on a node, call a perform…() function:

composeTestRule.onNode(...).performClick()

Here are some examples of actions:

performClick(),
performSemanticsAction(key),
performKeyPress(keyEvent),
performGesture { swipeLeft() }

You can browse the complete list in the Compose Testing cheat sheet.

Matchers

This section describes some of the matchers available for testing your Compose code.

Hierarchical matchers

Hierarchical matchers let you go up or down the semantics tree and perform simple matching.

fun hasParent(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnySibling(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyAncestor(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyDescendant(matcher: SemanticsMatcher):  SemanticsMatcher

Here are some examples of these matchers being used:

composeTestRule.onNode(hasParent(hasText("Button")))
    .assertIsDisplayed()

Selectors

An alternative way to create tests is to use selectors which can make some tests more readable.

composeTestRule.onNode(hasTestTag("Players"))
    .onChildren()
    .filter(hasClickAction())
    .assertCountEquals(4)
    .onFirst()
    .assert(hasText("John"))

You can browse the complete list in the Compose Testing cheat sheet.

Synchronization

Compose tests are synchronized by default with your UI. When you call an assertion or an action via the ComposeTestRule, the test will be synchronized beforehand, waiting until the UI tree is idle.

Normally, you don't have to take any action. However, there are some edge cases you should know about.

When a test is synchronized, your Compose app is advanced in time using a virtual clock. This means Compose tests don't run in real time, so they can pass as fast as possible.

However, if you don't use the methods that synchronize your tests, no recomposition will occur and the UI will appear to be paused.

@Test
fun counterTest() {
    val myCounter = mutableStateOf(0) // State that can cause recompositions
    var lastSeenValue = 0 // Used to track recompositions
    composeTestRule.setContent {
        Text(myCounter.value.toString())
        lastSeenValue = myCounter.value
    }
    myCounter.value = 1 // The state changes, but there is no recomposition

    // Fails because nothing triggered a recomposition
    assertTrue(lastSeenValue == 1)

    // Passes because the assertion triggers recomposition
    composeTestRule.onNodeWithText("1").assertExists()
}

It's also important to note that this requirement only applies to Compose hierarchies, and not to the rest of the app.

Disabling automatic synchronization

When you call an assertion or action through the ComposeTestRule such as assertExists(), your test is synchronized with the Compose UI. In some cases you might want to stop this synchronization and control the clock yourself. For example, you can control time to take accurate screenshots of an animation at a point where the UI would still be busy. To disable automatic synchronization, set the autoAdvance property in the mainClock to false:

composeTestRule.mainClock.autoAdvance = false

Typically you will then advance the time yourself. You can advance exactly one frame with advanceTimeByFrame() or by a specific duration with advanceTimeBy():

composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)

Idling resources

Compose can synchronize tests and the UI so that every action and assertion is done in an idle state, waiting or advancing the clock as needed. However, some asynchronous operations whose results affect the UI state can be run in the background while the test is unaware of them.

You can create and register these idling resources in your test so that they're taken into account when deciding whether the app under test is busy or idle. You don't have to take action unless you need to register additional idling resources, for example, if you run a background job that is not synchronized with Espresso or Compose.

This API is very similar to Espresso's Idling Resources to indicate if the subject under test is idle or busy. You use the Compose test rule to register the implementation of the IdlingResource.

composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)

Manual synchronization

In certain cases, you have to synchronize the Compose UI with other parts of your test or the app you're testing.

waitForIdle waits for Compose to be idle, but it depends on the autoAdvance property:

composeTestRule.mainClock.autoAdvance = true // default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle

composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for Idling Resources to become idle

Note that in both cases, waitForIdle will also wait for pending draw and layout passes.

Also, you can advance the clock until a certain condition is met with advanceTimeUntil().

composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }

Note that the given condition should be checking the state that can be affected by this clock (it only works with Compose state).

Waiting for conditions

Any condition that depends on external work, such as data loading or Android's measure or draw (that is, measure or draw external to Compose), should use a more general concept such as waitUntil():

composeTestRule.waitUntil(timeoutMs) { condition }

You can also use any of the waitUntil helpers:

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

Common patterns

This section describes some common approaches you'll see in Compose testing.

Test in isolation

ComposeTestRule lets you start an activity displaying any composable: your full application, a single screen, or a small element. It's also a good practice to check that your composables are correctly encapsulated and they work independently, allowing for easier and more focused UI testing.

This doesn't mean you should only create unit UI tests. UI tests scoping larger parts of your UI are also very important.

Access the activity and resources after setting your own content

Oftentimes you need to set the content under test using composeTestRule.setContent and you also need to access activity resources, for example to assert that a displayed text matches a string resource. However, you can't call setContent on a rule created with createAndroidComposeRule() if the activity already calls it.

A common pattern to achieve this is to create an AndroidComposeTestRule using an empty activity (such as ComponentActivity).

class MyComposeTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule<ComponentActivity>()

    @Test
    fun myTest() {
        // Start the app
        composeTestRule.setContent {
            MyAppTheme {
                MainScreen(uiState = exampleUiState, /*...*/)
            }
        }
        val continueLabel = composeTestRule.activity.getString(R.string.next)
        composeTestRule.onNodeWithText(continueLabel).performClick()
    }
}

Note that ComponentActivity needs to be added to your app's AndroidManifest.xml file. You can do that by adding this dependency to your module:

debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

Custom semantics properties

You can create custom semantics properties to expose information to tests. To do this, define a new SemanticsPropertyKey and make it available using the SemanticsPropertyReceiver.

// Creates a Semantics property of type Long
val PickedDateKey = SemanticsPropertyKey<Long>("PickedDate")
var SemanticsPropertyReceiver.pickedDate by PickedDateKey

Now you can use that property using the semantics modifier:

val datePickerValue by remember { mutableStateOf(0L) }
MyCustomDatePicker(
    modifier = Modifier.semantics { pickedDate = datePickerValue }
)

From tests, you can use SemanticsMatcher.expectValue to assert the value of the property:

composeTestRule
    .onNode(SemanticsMatcher.expectValue(PickedDateKey, 1445378400)) // 2015-10-21
    .assertExists()

Verify state restoration

You should verify that the state of your Compose elements is correctly restored when the activity or process is recreated. It's possible to perform such a check without relying on activity recreation with the StateRestorationTester class.

This class lets you simulate the recreation of a composable. It's especially useful to verify the implementation of rememberSaveable.


class MyStateRestorationTests {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun onRecreation_stateIsRestored() {
        val restorationTester = StateRestorationTester(composeTestRule)

        restorationTester.setContent { MainScreen() }

        // TODO: Run actions that modify the state

        // Trigger a recreation
        restorationTester.emulateSavedInstanceStateRestore()

        // TODO: Verify that state has been correctly restored.
    }
}

Test different device configurations

Android apps need to adapt to many changing conditions: window sizes, locales, font sizes, dark and light themes, and more. Most of these conditions are derived from device-level values controlled by the user and exposed with the current Configuration instance. Testing different configurations directly in a test is difficult since the test must configure device-level properties.

DeviceConfigurationOverride is a test-only API that lets you simulate different device configurations in a localized way for the @Composable content under test.

The companion object of DeviceConfigurationOverride has the following extension functions, which override device-level configuration properties:

To apply a specific override, wrap the content under test in a call to the DeviceConfigurationOverride() top-level function, passing the override to apply as a parameter.

For example, the following code applies the DeviceConfigurationOverride.ForcedSize() override to change the density locally, forcing the MyScreen composable to be rendered in a large landscape window, even if the device the test is running on doesn't support that window size directly:

composeTestRule.setContent {
    DeviceConfigurationOverride(
        DeviceConfigurationOverride.ForcedSize(DpSize(1280.dp, 800.dp))
    ) {
        MyScreen() // will be rendered in the space for 1280dp by 800dp without clipping
    }
}

To apply multiple overrides together, use DeviceConfigurationOverride.then():

composeTestRule.setContent {
    DeviceConfigurationOverride(
        DeviceConfigurationOverride.FontScale(1.5f) then
            DeviceConfigurationOverride.FontWeightAdjustment(200)
    ) {
        Text(text = "text with increased scale and weight")
    }
}

Debugging

The main way to solve problems in your tests is to look at the semantics tree. You can print the tree by calling composeTestRule.onRoot().printToLog() at any point in your test. This function prints a log like this:

Node #1 at (...)px
 |-Node #2 at (...)px
   OnClick = '...'
   MergeDescendants = 'true'
    |-Node #3 at (...)px
    | Text = 'Hi'
    |-Node #5 at (83.0, 86.0, 191.0, 135.0)px
      Text = 'There'

These logs contain valuable information for tracking bugs down.

Interoperability with Espresso

In a hybrid app you can find Compose components inside view hierarchies and views inside Compose composables (via the AndroidView composable).

There are no special steps needed to match either type. You match views via Espresso's onView, and Compose elements via the ComposeTestRule.

@Test
fun androidViewInteropTest() {
    // Check the initial state of a TextView that depends on a Compose state:
    Espresso.onView(withText("Hello Views")).check(matches(isDisplayed()))
    // Click on the Compose button that changes the state
    composeTestRule.onNodeWithText("Click here").performClick()
    // Check the new value
    Espresso.onView(withText("Hello Compose")).check(matches(isDisplayed()))
}

Interoperability with UiAutomator

By default, composables are accessible from UiAutomator only by their convenient descriptors (displayed text, content description, etc.). If you want to access any composable that uses Modifier.testTag, you need to enable the semantic property testTagsAsResourceId for the particular composables subtree. Enabling this behavior is useful for composables that don't have any other unique handle, such as scrollable composables (for example, LazyColumn).

You can enable it only once high in your composables hierarchy to ensure all of nested composables with Modifier.testTag are accessible from UiAutomator.

Scaffold(
    // Enables for all composables in the hierarchy.
    modifier = Modifier.semantics {
        testTagsAsResourceId = true
    }
){
    // Modifier.testTag is accessible from UiAutomator for composables nested here.
    LazyColumn(
        modifier = Modifier.testTag("myLazyColumn")
    ){
        // content
    }
}

Any composable with the Modifier.testTag(tag) can be accessible with the use of By.res(resourceName) using the same tag as the resourceName.

val device = UiDevice.getInstance(getInstrumentation())

val lazyColumn: UiObject2 = device.findObject(By.res("myLazyColumn"))
// some interaction with the lazyColumn

Learn more

To learn more try the Jetpack Compose Testing codelab.

Samples