Learn Jetpack Compose testing from beginner to interview level: Compose test rules, semantics tree, node finders, matchers, actions, assertions, lazy-list testing, navigation, animations, mixed View/Compose screens, accessibility, robot pattern and end-to-end testing.

Required Dependencies

dependencies {
    androidTestImplementation(
        platform(libs.compose.bom)
    )

    androidTestImplementation(
        libs.compose.ui.test.junit4
    )

    debugImplementation(
        libs.compose.ui.test.manifest
    )
}

Typical equivalent:

androidTestImplementation(
    "androidx.compose.ui:ui-test-junit4:<version>"
)

debugImplementation(
    "androidx.compose.ui:ui-test-manifest:<version>"
)

---

25. Compose Testing Fundamentals

Compose tests interact with the semantics tree, not directly with composable functions.

Basic flow:

flowchart LR
    F["Find semantics node"]
    A["Perform action"]
    V["Verify result"]

    F --> A --> V

Example

class LoginScreenTest {

    @get:Rule
    val composeRule = createComposeRule()

    @Test
    fun validCredentials_clickLogin_showsWelcome() {
        composeRule.setContent {
            LoginScreen()
        }

        composeRule
            .onNodeWithTag("email_input")
            .performTextInput("user@example.com")

        composeRule
            .onNodeWithTag("password_input")
            .performTextInput("Password123")

        composeRule
            .onNodeWithText("Login")
            .performClick()

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

Compose testing APIs provide:

* Finders

* Matchers

* Actions

* Assertions

* Synchronization

* Test clock

* Semantics inspection

* View interoperability

Official Compose testing overview.

---

26. Compose Test Rules

## `createComposeRule()`

Use when testing a composable in isolation and Activity access is unnecessary.

@get:Rule
val composeRule = createComposeRule()

createAndroidComposeRule<Activity>()

Use when the test needs:

* Activity

* Android resources

* Intent

* Activity lifecycle

* Production screen integration

@get:Rule
val composeRule =
    createAndroidComposeRule<MainActivity>()

Access the Activity:

val label = composeRule.activity
    .getString(R.string.login)

Rule comparison

RuleBest use
createComposeRule()Isolated composable
createAndroidComposeRule<ComponentActivity>()Isolated content plus Activity/resources
createAndroidComposeRule<MainActivity>()Existing Activity integration
runComposeUiTestModern Compose testing with synchronized coroutine/test clock

When the Activity already calls setContent, do not call setContent again from the test.

For coroutine-heavy UI tests, current Compose guidance recommends `runComposeUiTest` to avoid separate Compose and coroutine schedulers. [Compose testing API migration](https://developer.android.com/develop/ui/compose/testing/migrate-v2).

---

27. Setting Test Content

Use setContent to provide the composable under test.

@Test
fun emptyTaskList_displaysEmptyMessage() {
    composeRule.setContent {
        AppTheme {
            TaskListScreen(
                state = TaskUiState.Empty,
               
            )
        }
    }

    composeRule
        .onNodeWithText("No tasks available")
        .assertIsDisplayed()
}

Test with fake state

@Test
fun successState_displaysTasks() {
    val state = TaskUiState.Success(
        tasks = listOf(
            Task(1, "Learn Compose Testing")
        )
    )

    composeRule.setContent {
        TaskListScreen(
            state = state,
           
        )
    }

    composeRule
        .onNodeWithText("Learn Compose Testing")
        .assertIsDisplayed()
}

Testing stateless screens with explicit state and callbacks is easier than constructing the full application.

Recommended screen design

@Composable
fun TaskRoute(
    viewModel: TaskViewModel,
    onNavigate: (Long) -> Unit
) {
    val state by viewModel.state.collectAsStateWithLifecycle()

    TaskScreen(
        state = state,
       
    )
}

Test TaskScreen in isolation and keep a smaller number of TaskRoute integration tests.

---

28. Compose Semantics Tree

The semantics tree describes the meaning and supported actions of the UI.

It can contain:

* Text

* Content description

* Role

* State description

* Selected state

* Enabled state

* Click action

* Scroll action

* Editable text

* Test tag

flowchart TD
    ROOT["Screen"]
    FORM["Login form"]
    EMAIL["Email field"]
    PASS["Password field"]
    BUTTON["Login button"]

    ROOT --> FORM
    FORM --> EMAIL
    FORM --> PASS
    FORM --> BUTTON

Add semantics

Button(
    modifier = Modifier.semantics {
        contentDescription = "Submit login"
    },
   
) {
    Text("Login")
}

Add test tag

TextField(
    modifier = Modifier.testTag("email_input"),
    value = email,
   
)

Print semantics tree

composeRule
    .onRoot()
    .printToLog("COMPOSE_TREE")

Print the tree when a node cannot be found or multiple nodes match unexpectedly.

Compose testing and accessibility both use semantics. Compose semantics guide.

---

29. Merged vs Unmerged Semantics Tree

Some composables merge their children’s semantics.

Example:

Button(onClick = {}) {
    Icon(
        imageVector = Icons.Default.Save,
        contentDescription = null
    )
    Text("Save")
}

The merged tree may expose one Button node containing:

Role = Button
Text = Save
OnClick

The individual Icon and Text nodes may not be independently visible in the merged tree.

Merged tree

Default:

composeRule.onNodeWithText("Save")

Represents the accessibility-friendly logical UI.

Unmerged tree

composeRule.onNodeWithText(
    text = "Save",
    useUnmergedTree = true
)

Use when you specifically need to inspect a child hidden by semantic merging.

Comparison

TreePurpose
MergedUser/accessibility-level behaviour
UnmergedInternal child semantics and debugging
DefaultMerged tree
RecommendedStart merged; use unmerged only when needed

Do not use the unmerged tree only to compensate for poor semantics.

---

30. Finding Nodes

Single node

composeRule.onNode(
    hasText("Login")
)

Convenience form:

composeRule.onNodeWithText("Login")

Multiple nodes

composeRule.onAllNodesWithText("Delete")

Content description

composeRule.onNodeWithContentDescription(
    "Open settings"
)

Hierarchical matching

composeRule.onNode(
    hasText("Delete") and
        hasAnyAncestor(
            hasTestTag("task_10")
        )
)

Useful hierarchical matchers:

* `hasParent()`
* `hasAnyChild()`
* `hasAnyAncestor()`
* `hasAnyDescendant()`
* `hasAnySibling()`

Node collection

composeRule
    .onAllNodes(hasClickAction())
    .assertCountEquals(3)

Compose exposes finders for single and multiple semantics nodes. Compose testing APIs.

---

31. Text and Tag Matchers

Text matcher

composeRule
    .onNodeWithText("Login")
    .assertExists()

Options:

onNodeWithText(
    text = "login",
    substring = false,
    ignoreCase = true
)

Test-tag matcher

Production:

Modifier.testTag("login_button")

Test:

composeRule
    .onNodeWithTag("login_button")
    .assertIsDisplayed()

When to use text

Use text when it represents user-observable behaviour.

When to use test tags

Use tags for:

* Text fields without stable visible labels

* Lazy lists

* Duplicate elements

* Complex custom components

* Non-text elements

* Stable test hooks

Do not tag every composable. Prefer text, content descriptions, roles and state where they express real user behaviour.

---

32. Performing Click and Text Actions

Click

composeRule
    .onNodeWithText("Login")
    .performClick()

Text input

composeRule
    .onNodeWithTag("email_input")
    .performTextInput("user@example.com")

Replace text

composeRule
    .onNodeWithTag("email_input")
    .performTextReplacement("new@example.com")

Clear text

composeRule
    .onNodeWithTag("email_input")
    .performTextClearance()

IME action

composeRule
    .onNodeWithTag("search_input")
    .performImeAction()

Gesture

composeRule
    .onNodeWithTag("task_card")
    .performTouchInput {
        swipeLeft()
    }

Other actions include:

* `performScrollTo()`
* `performScrollToIndex()`
* `performScrollToNode()`
* `performKeyInput()`
* `performSemanticsAction()`

The node must expose the semantics action being performed.

---

33. Assertions in Compose Tests

Existence

node.assertExists()
node.assertDoesNotExist()

Visibility

node.assertIsDisplayed()
node.assertIsNotDisplayed()

State

node.assertIsEnabled()
node.assertIsNotEnabled()
node.assertIsSelected()
node.assertIsNotSelected()
node.assertIsOn()
node.assertIsOff()

Text

node.assertTextEquals("Login")
node.assertTextContains("Welcome")

Content description

node.assertContentDescriptionEquals(
    "Open settings"
)

Actions

node.assertHasClickAction()
node.assertHasNoClickAction()

Collection assertions

composeRule
    .onAllNodesWithTag("task_item")
    .assertCountEquals(3)

Test behaviour rather than implementation styling.

Avoid assertions such as exact padding, internal modifier order or private composable structure unless they are genuine requirements.

---

34. Lists and Scrolling Tests

Compose lazy lists expose semantic scrolling operations.

Example LazyColumn

LazyColumn(
    modifier = Modifier.testTag("task_list")
) {
    items(
        items = tasks,
        key = { it.id }
    ) { task ->
        TaskItem(
            modifier = Modifier.testTag(
                "task_${task.id}"
            ),
            task = task
        )
    }
}

Scroll to item by matcher

composeRule
    .onNodeWithTag("task_list")
    .performScrollToNode(
        hasText("Task 50")
    )

Scroll to index

composeRule
    .onNodeWithTag("task_list")
    .performScrollToIndex(49)

Assert item

composeRule
    .onNodeWithText("Task 50")
    .assertIsDisplayed()

Verify number of composed nodes carefully

Lazy layouts compose only visible or nearby items. Therefore:

onAllNodesWithTag("task_item")

may not represent the entire dataset.

Test:

* Empty list

* First item

* Off-screen item

* Scrolling

* Item action

* Pagination indicator

* Loading footer

* Stable item identity

* List refresh

* Empty search results

---

35. Navigation Testing

Use two levels of navigation testing.

Level 1: Screen callback test

Verify the screen produces the correct navigation request.

@Test
fun taskClicked_callsNavigationWithTaskId() {
    var navigatedTaskId: Long? = null

    composeRule.setContent {
        TaskScreen(
            state = TaskUiState.Success(
                listOf(Task(10, "Testing"))
            ),
           
                navigatedTaskId = it
            }
        )
    }

    composeRule
        .onNodeWithText("Testing")
        .performClick()

    composeRule.runOnIdle {
        assertEquals(
            10L,
            navigatedTaskId
        )
    }
}

Level 2: NavHost integration test

Use a test navigation controller when verifying navigation graph integration.

lateinit var navController: TestNavHostController

composeRule.setContent {
    navController =
        TestNavHostController(
            LocalContext.current
        ).apply {
            navigatorProvider.addNavigator(
                ComposeNavigator()
            )
        }

    AppNavHost(navController)
}
composeRule
    .onNodeWithText("Testing")
    .performClick()

composeRule.runOnIdle {
    assertEquals(
        "task/10",
        navController.currentBackStackEntry
            ?.destination
            ?.route
    )
}

Test:

* Correct destination

* Arguments

* Back navigation

* Deep links where applicable

* Login-gated navigation

* Back-stack behaviour

Do not use a full NavHost test when a callback test provides enough confidence.

---

36. Compose Clock and Animation Testing

Compose tests automatically synchronize with the UI and use a test clock.

Disable automatic advancement

composeRule.mainClock.autoAdvance = false

Advance time

composeRule.mainClock.advanceTimeBy(500)

Advance one frame

composeRule.mainClock.advanceTimeByFrame()

Animation example

@Test
fun progressAnimation_reachesFinalState() {
    composeRule.mainClock.autoAdvance = false

    composeRule.setContent {
        AnimatedProgress(
            targetProgress = 1f
        )
    }

    composeRule.mainClock.advanceTimeBy(1_000)
    composeRule.waitForIdle()

    composeRule
        .onNodeWithTag("progress")
        .assert(
            SemanticsMatcher.expectValue(
                ProgressKey,
                1f
            )
        )
}

Important distinction

MainTestClock controls:

* Recomposition

* Compose animations

* Gestures driven by Compose time

It does not directly control every Android View measure/draw operation or external background job.

Compose automatically synchronizes before most actions and assertions. Compose synchronization.

---

37. Testing Mixed Compose and View Screens

Hybrid applications may contain:

* Compose inside ComposeView

* Android Views inside AndroidView

* XML screens gradually migrating to Compose

Use:

* ComposeTestRule for Compose semantics

* Espresso for Android Views

Example

@Test
fun composeButton_updatesLegacyTextView() {
    onView(withText("Hello Views"))
        .check(matches(isDisplayed()))

    composeRule
        .onNodeWithText("Update")
        .performClick()

    onView(withText("Hello Compose"))
        .check(matches(isDisplayed()))
}

Android View inside Compose

AndroidView(
    factory = { context ->
        TextView(context).apply {
            id = R.id.legacyText
        }
    }
)

Test it with Espresso:

onView(withId(R.id.legacyText))
    .check(matches(withText("Legacy content")))

Normally, no special bridge is required: use each testing API for its corresponding UI system. Compose testing interoperability.

---

38. Accessibility and Semantics Testing

Compose semantics are used by both testing tools and accessibility services.

Important properties include:

* contentDescription

* role

* stateDescription

* selected

* disabled

* heading

* onClick

* toggleableState

* Collection information

Content description test

composeRule
    .onNodeWithContentDescription(
        "Open profile"
    )
    .assertIsDisplayed()
    .assertHasClickAction()

Toggle test

composeRule
    .onNodeWithText("Notifications")
    .assertIsOff()
    .performClick()
    .assertIsOn()

Role test

composeRule
    .onNode(
        hasText("Submit") and
            hasRole(Role.Button)
    )
    .assertIsDisplayed()

Custom component

Modifier.semantics {
    role = Role.Button
    contentDescription = "Pay ₹1,000"
    stateDescription = "Ready"
}

Accessibility tests should verify:

* Images have meaningful descriptions when needed

* Decorative images do not create noise

* Clickable elements expose actions

* Toggle state is announced

* Errors are discoverable

* Controls have appropriate roles

* Important headings are represented

* Custom components expose meaningful semantics

Automated semantics tests help, but they do not replace manual TalkBack and accessibility evaluation.

---

39. UI Test Robots and Page Objects

A robot/page object encapsulates screen-specific test operations.

Robot

class LoginRobot(
    private val rule: ComposeTestRule
) {

    fun enterEmail(email: String) = apply {
        rule.onNodeWithTag("email_input")
            .performTextInput(email)
    }

    fun enterPassword(password: String) = apply {
        rule.onNodeWithTag("password_input")
            .performTextInput(password)
    }

    fun clickLogin() = apply {
        rule.onNodeWithText("Login")
            .performClick()
    }

    fun verifyError(message: String) = apply {
        rule.onNodeWithText(message)
            .assertIsDisplayed()
    }
}

Usage

@Test
fun invalidPassword_displaysError() {
    composeRule.setContent {
        LoginScreen()
    }

    LoginRobot(composeRule)
        .enterEmail("user@example.com")
        .enterPassword("123")
        .clickLogin()
        .verifyError("Password is too short")
}

Advantages

* Reduces repeated test code

* Centralizes matchers

* Improves readability

* Simplifies UI changes

* Creates a screen-focused testing language

Risks

* Giant robots covering many screens

* Assertions hidden too deeply

* Complex inheritance

* Robot reproduces application logic

* Tests become difficult to debug

Keep robots small, composable and behaviour-focused.

---

40. End-to-End User Journeys

An E2E test verifies a complete user journey across multiple layers.

Example payment journey

flowchart TD
    L["Login"]
    B["Select beneficiary"]
    A["Enter amount"]
    C["Confirm payment"]
    P["Payment processing"]
    R{"Result"}
    S["Success receipt"]
    E["Pending/error state"]

    L --> B --> A --> C --> P --> R
    R -->|Success| S
    R -->|Pending/failure| E

Good E2E candidates

* Registration → login

* Login → home

* Search → details

* Add task → restart → task remains

* Add beneficiary → payment confirmation

* Offline creation → later synchronization

* Session expiry → re-authentication

Payment E2E scenarios

1. Successful payment

2. Insufficient balance

3. Duplicate confirmation tap

4. Timeout leading to pending status

5. Server rejection

6. Process/recreation state restoration

7. Receipt display

Principles

* Keep E2E tests few

* Focus on critical business journeys

* Control backend/test data

* Avoid dependence on production services

* Reset application state

* Capture screenshots and logs on failure

* Use stable semantics

* Monitor flakiness

* Run selected journeys in CI

* Test most business rules below the UI layer

---

Complete Compose Use Case: Task Screen

Screen states

stateDiagram-v2
    [*] --> Loading
    Loading --> Success: Tasks received
    Loading --> Empty: No tasks
    Loading --> Error: Request fails
    Error --> Loading: Retry
    Success --> Details: Task clicked

Testing matrix

BehaviourTest level
Initial loading indicatorCompose UI
Tasks displayedCompose UI
Empty messageCompose UI
Error messageCompose UI
Retry callbackCompose UI
Task click callbackCompose UI
Correct destinationNavigation integration
Off-screen taskLazy-list test
Loading animationTest clock
Accessible task actionSemantics test
View/Compose interactionHybrid test
Create and reopen taskE2E

---

UI-Testing Common Mistakes

1. Testing every business rule through UI

2. Creating too many E2E tests

3. Depending on production backend

4. Using real user credentials

5. Using Thread.sleep()

6. Using arbitrary long timeouts

7. Matching unstable text unnecessarily

8. Adding test tags to every composable

9. Depending on internal composable hierarchy

10. Using the unmerged tree without understanding why

11. Forgetting StateFlow’s initial UI state

12. Asserting before the UI becomes idle

13. Mixing runTest and Compose clocks incorrectly

14. Not controlling animations

15. Assuming Compose clock controls every Android operation

16. Counting every item in a lazy list as if all were composed

17. Using list index when stable content is available

18. Testing navigation only with a full application

19. Directly accessing private screen state

20. Testing exact padding or modifier order

21. Ignoring accessibility semantics

22. Using content descriptions as visual labels incorrectly

23. Assuming automated accessibility tests replace TalkBack

24. Creating one giant robot for the whole application

25. Hiding all assertions inside complicated page objects

26. Sharing mutable state between tests

27. Not resetting fake repositories

28. Allowing tests to depend on execution order

29. Ignoring screen-size differences

30. Ignoring failure screenshots and logs

31. Not testing empty and error states

32. Not testing duplicate clicks

33. Running all UI tests on one API/device configuration

34. Using E2E retries to hide real flakiness

---

Interview Questions and Answers

1. How are Compose UIs tested?

Compose tests find nodes in the semantics tree, perform actions and make assertions.

2. Does a Compose test interact directly with composable functions?

No. It generally interacts with semantics nodes generated by the composed UI.

3. What is createComposeRule()?

It creates a Compose test environment for testing composables in isolation.

4. What is createAndroidComposeRule()?

It creates a Compose test rule with access to a specified Android Activity.

5. When should runComposeUiTest be considered?

For modern tests needing coordinated Compose and coroutine scheduling without managing separate clocks manually.

6. What does setContent do?

It installs the composable hierarchy that the test will interact with.

7. What is the semantics tree?

It is a tree describing UI meaning, properties, state and supported actions.

8. Why does Compose testing use semantics?

Not every composable represents a concrete View. Semantics provide a stable, meaningful testing and accessibility interface.

9. What is semantic merging?

A parent combines its children’s semantic properties into one logical node.

10. Merged vs unmerged semantics tree?

The merged tree represents logical user-facing elements. The unmerged tree exposes lower-level child nodes.

11. Which tree is used by default?

The merged semantics tree.

12. How do you inspect the semantics tree?

composeRule.onRoot().printToLog("TAG")

13. How do you find a Compose node?

Use APIs such as onNode, onNodeWithText, onNodeWithTag or onNodeWithContentDescription.

14. onNode vs onAllNodes?

onNode expects a single matching node. onAllNodes returns a collection.

15. Text matcher vs test tag?

Text represents user-visible behaviour. Test tags provide stable identifiers when meaningful semantics are insufficient.

16. Should every composable have a test tag?

No. Prefer user-visible text, content descriptions, roles and state where possible.

17. How do you click a Compose node?

onNodeWithText("Login").performClick()

18. How do you enter text?

onNodeWithTag("email").performTextInput("user@example.com")

19. What are common Compose assertions?

Existence, display, enabled state, selection, text, content description and click-action assertions.

20. How do you test an off-screen LazyColumn item?

Use performScrollToNode() or performScrollToIndex() and then assert the item.

21. Why can’t you count all LazyColumn items through current semantics nodes?

Lazy layouts compose only visible and nearby items.

22. How should navigation be tested?

Test screen callbacks in isolation and use selected NavHost integration tests for destinations, arguments and back stack.

23. What is TestNavHostController?

A navigation controller designed to inspect and control navigation in tests.

24. What is Compose’s main test clock?

A virtual clock that controls recomposition, Compose animations and related frame-driven work.

25. How do you manually control animation time?

Disable mainClock.autoAdvance and call advanceTimeBy() or advanceTimeByFrame().

26. Does Compose’s test clock control all Android operations?

No. Android View drawing and external background operations may need separate synchronization.

27. Are Compose tests synchronized automatically?

Yes. Actions and assertions generally wait for Compose to become idle.

28. How do you test a mixed Compose and View screen?

Use ComposeTestRule for Compose nodes and Espresso for Android Views.

29. What is accessibility semantics testing?

It verifies that controls expose meaningful descriptions, roles, states and actions.

30. Do semantics tests replace manual TalkBack testing?

No. Manual accessibility evaluation remains necessary.

31. What is the robot pattern?

A pattern that encapsulates screen actions and assertions behind a readable test API.

32. What is the benefit of test robots?

They reduce duplication and isolate selectors from test scenarios.

33. What is a risk of robots/page objects?

They can become large, hide failures and duplicate application logic.

34. What is an end-to-end test?

A test that verifies a complete user journey across UI, business and data layers.

35. Why should E2E tests be limited?

They are slow, expensive, difficult to debug and more likely to become flaky.

36. What should be covered by E2E tests?

A small set of critical business journeys rather than every validation rule.

37. How do you make Compose UI tests reliable?

Use controlled dependencies, stable semantics, automatic synchronization, virtual time and isolated test state.

38. What is the best Compose testing principle?

Test user-observable behaviour through meaningful semantics rather than internal composable implementation.

---

Quick Revision

Compose test:
Find node → Perform action → Assert result

createComposeRule:
Isolated composable

createAndroidComposeRule:
Activity integration

Semantics:
Meaning, state and actions exposed by UI

Merged tree:
Logical accessible nodes

Unmerged tree:
Internal child semantics

Find:
onNodeWithText()
onNodeWithTag()
onNodeWithContentDescription()

Act:
performClick()
performTextInput()
performScrollToNode()

Assert:
assertExists()
assertIsDisplayed()
assertIsEnabled()
assertTextEquals()

LazyColumn:
Only visible/nearby items are composed

Navigation:
Screen callback test + selected NavHost test

Animation:
mainClock.autoAdvance
advanceTimeBy()

Hybrid screen:
Compose Test + Espresso

E2E:
Few critical user journeys