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 --> VExample
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
| Rule | Best use |
|---|---|
createComposeRule() | Isolated composable |
createAndroidComposeRule<ComponentActivity>() | Isolated content plus Activity/resources |
createAndroidComposeRule<MainActivity>() | Existing Activity integration |
runComposeUiTest | Modern 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 --> BUTTONAdd 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
OnClickThe 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
| Tree | Purpose |
|---|---|
| Merged | User/accessibility-level behaviour |
| Unmerged | Internal child semantics and debugging |
| Default | Merged tree |
| Recommended | Start 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 = falseAdvance 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| EGood 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 clickedTesting matrix
| Behaviour | Test level |
|---|---|
| Initial loading indicator | Compose UI |
| Tasks displayed | Compose UI |
| Empty message | Compose UI |
| Error message | Compose UI |
| Retry callback | Compose UI |
| Task click callback | Compose UI |
| Correct destination | Navigation integration |
| Off-screen task | Lazy-list test |
| Loading animation | Test clock |
| Accessible task action | Semantics test |
| View/Compose interaction | Hybrid test |
| Create and reopen task | E2E |
---
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