Expert: features and shipping · Testing Android apps · topic 41 of 50 · 30 min
Compose UI tests
Explanation
What a UI test does
A UI test drives your screen like a user: it types into fields, taps buttons and checks what appears. Compose UI tests read the semantics tree (the same tree TalkBack uses), so they find elements by their text, content description or test tag, not by pixel position. They usually live in src/androidTest and run on a device or emulator.
The pieces
| Kind | Examples |
|---|---|
| Rule | createComposeRule() (no activity needed) or createAndroidComposeRule<MainActivity>() |
| Finders | onNodeWithText("Pay"), onNodeWithTag("total"), onNodeWithContentDescription("Add to cart"), onAllNodesWithText(...) |
| Actions | performClick(), performTextInput("98765"), performTextClearance(), performScrollToNode(...) |
| Assertions | assertIsDisplayed(), assertIsEnabled(), assertIsNotEnabled(), assertTextEquals("Rs 118"), assertDoesNotExist() |
A complete test
// The composable under test (stateless, from module 6)
@Composable
fun SignupForm(onSubmit: (String) -> Unit) {
var mobile by remember { mutableStateOf("") }
Column {
OutlinedTextField(mobile, { mobile = it.filter(Char::isDigit).take(10) },
label = { Text("Mobile number") }, modifier = Modifier.testTag("mobile"))
Button(onClick = { onSubmit(mobile) }, enabled = mobile.length == 10) { Text("Continue") }
}
}
// app/src/androidTest/.../SignupFormTest.kt
class SignupFormTest {
@get:Rule val rule = createComposeRule()
@Test
fun continueEnabledOnlyWithTenDigits() {
var submitted = ""
rule.setContent { SignupForm(onSubmit = { submitted = it }) }
rule.onNodeWithText("Continue").assertIsNotEnabled()
rule.onNodeWithTag("mobile").performTextInput("98765abc43210")
rule.onNodeWithText("Continue").assertIsEnabled().performClick()
assertEquals("9876543210", submitted)
}
}testTag
When text is not unique or changes with language, give the element a stable test tag with Modifier.testTag("total") and find it with onNodeWithTag. Tags do not affect what users see. Keep them as constants so tests and UI use the same string.
Synchronisation
Compose tests automatically wait until the UI is idle (no pending recompositions or animations) before each finder and assertion, so you rarely need waits. For work outside Compose's knowledge, rule.waitUntil { condition } waits for a condition with a timeout. Never use Thread.sleep in tests.
Test stateless content
UI tests are easiest on stateless composables that take state and lambdas (the "Content" pattern from module 7). You can show any state, even rare error states, without a real ViewModel, server or database, and check that the UI shows the right thing.
Because finders use semantics, good accessibility and good testability go together: a button with a clear contentDescription is easy for TalkBack and easy for tests.
Espresso, briefly
Older apps built with XML Views use Espresso for UI tests (onView(withId(R.id.pay)).perform(click())). Compose and Espresso can be mixed in apps that contain both kinds of UI. New Compose code should use the Compose testing APIs.
Common mistakes
- Finding by text that will be translated, so tests fail in Hindi. Use test tags or string resources.
- Testing through the whole app (login, navigation) just to check one button. Set the composable directly.
- Using Thread.sleep instead of letting Compose synchronise.
- Too many slow UI tests for logic that a unit test could cover.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Compose UI tests · 1/6
Compose UI tests drive a screen like a user, using the semantics tree.
Prefer a video by a teacher? Find free videos on “jetpack compose ui testing createComposeRule onNodeWithText performClick” on YouTube ↗
In real life: Guarding the checkout button
An e-commerce team once shipped a bug where the Pay button stayed enabled with an empty cart, creating zero-rupee orders. They added a Compose UI test that sets CheckoutContent with an empty cart and asserts the Pay button is not enabled, and another with two items that asserts the total text equals "Rs 1,298". The bug can never silently return.
@Test
fun payDisabledForEmptyCart() {
rule.setContent { CheckoutContent(state = CheckoutUiState(items = emptyList()), onPay = {}) }
rule.onNodeWithText("Pay").assertIsNotEnabled()
}Practice (0/3 done)
Which call types "560001" into a text field found by its tag "pin"?
Why add Modifier.testTag("total") to a Text?
Write Compose UI tests for a QuantityPicker(qty, onQtyChange) and a CartLine that shows the total: (1) the minus button does not go below 1, (2) tapping plus twice from 1 shows total Rs 360 for price 120, (3) the total has a testTag and assertTextEquals checks it.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Debugging and performanceTesting Android apps · same module
- Testing basics and the test pyramidTesting Android apps · same module
- State and recompositionJetpack Compose UI
- ViewModel and MVVMNavigation and app architecture
- Accessibility and adaptive layoutsPermissions, Material 3 and polish
- Widget testsFlutter and Dart · Responsive design and testing
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What do Compose UI tests use to find elements?
Answer: The semantics tree
Finders query semantics: text, tags, descriptions.
Which assertion checks a button can be tapped?
Answer: assertIsEnabled()
assertIsEnabled checks the enabled state.
Which tool tests older XML View screens?
Answer: Espresso
Espresso is the View-based UI testing framework.
Why test stateless "Content" composables directly?
Answer: You can show any state without a real ViewModel, server or database
Easy setup of exact states.
Which function shows Compose content in an activity?Related
What is a Modifier?Related
Answer: A chain of instructions that changes a composable's look or behaviour
Modifiers decorate and add behaviour.
Which survives screen rotation?Related
Answer: rememberSaveable
rememberSaveable stores the value in saved state.
What does the ViewModel expose to the UI?Related
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 1 more topic in “Testing Android apps” before its exam: Debugging and performance.
- Module 10 exam: Testing Android apps · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Publishing on the Play Store and Firebase. 50 topics left in the course before the final exam.