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 createComposeRule, but not createAndroidComposeRule:
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 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). Any condition that depends on Android's measure or draw (that is, measure or draw external to Compose) should use more general concept such as waitUntil():

composeTestRule.waitUntil(timeoutMs) { condition }

Common patterns

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

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

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 boolean
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()

Debugging

The main way to solve problems in your tests is to look at the semantics tree. You can print the tree by calling findRoot().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()))
}

Learn more

To learn more try the Jetpack Compose Testing codelab.