Skip to content

Expert: features and shipping · Testing Android apps · topic 41 of 50 · 30 min

Compose UI tests

Explanation

Compose UI tests set content on a test rule (createComposeRule), find nodes with finders such as onNodeWithText, onNodeWithTag and onNodeWithContentDescription, perform actions like performClick and performTextInput, and check them with assertions like assertIsDisplayed and assertIsEnabled. Add Modifier.testTag for elements without unique text. Espresso is the older tool for View-based screens.

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

KindExamples
RulecreateComposeRule() (no activity needed) or createAndroidComposeRule<MainActivity>()
FindersonNodeWithText("Pay"), onNodeWithTag("total"), onNodeWithContentDescription("Add to cart"), onAllNodesWithText(...)
ActionsperformClick(), performTextInput("98765"), performTextClearance(), performScrollToNode(...)
AssertionsassertIsDisplayed(), assertIsEnabled(), assertIsNotEnabled(), assertTextEquals("Rs 118"), assertDoesNotExist()

A complete test

kotlin
// 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)
    }
}
The test checks filtering, the enabled state and the callback, all through the UI.

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.

Think of it like this: A UI test is like a mystery shopper sent to your shop with a script: enter, ask for the price, try to pay with a wrong code, then the right one, and report exactly what happened at each step.

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

Find → Act → Assert

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.

kotlin
@Test
fun payDisabledForEmptyCart() {
    rule.setContent { CheckoutContent(state = CheckoutUiState(items = emptyList()), onPay = {}) }
    rule.onNodeWithText("Pay").assertIsNotEnabled()
}
One state, one assertion, no network.

Practice (0/3 done)

Task 1 · Question

Which call types "560001" into a text field found by its tag "pin"?

Task 2 · Question

Why add Modifier.testTag("total") to a Text?

Task 3 · Practice

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

Question 1

1.What do Compose UI tests use to find elements?

Question 2

2.Which assertion checks a button can be tapped?

Question 3

3.Which tool tests older XML View screens?

Question 4

4.Why test stateless "Content" composables directly?

Read more (free)

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

Answer: setContent

setContent { } hosts the composables.

From Hello Compose: your first screen

What is a Modifier?Related

Answer: A chain of instructions that changes a composable's look or behaviour

Modifiers decorate and add behaviour.

From Composables and modifiers

Which survives screen rotation?Related

Answer: rememberSaveable

rememberSaveable stores the value in saved state.

From State and recomposition

What does the ViewModel expose to the UI?Related

Answer: A read-only StateFlow of UI state

Expose read-only state.

From ViewModel and MVVM

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. 1 more topic in “Testing Android apps” before its exam: Debugging and performance.
  2. Module 10 exam: Testing Android apps · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Publishing on the Play Store and Firebase. 50 topics left in the course before the final exam.

← Previous: Unit tests with JUnit and coroutines