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.
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.
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 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.
}
}
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 testTagAsResourceId
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
Recommended for you
- Note: link text is displayed when JavaScript is off
- Semantics in Compose
- Window insets in Compose
- Other considerations