How to Confidently Test Jetpack Compose UI with Espresso
Source: Dev.to
If you’ve worked with Jetpack Compose long enough, you’ve probably thought:
“UI looks great — but how do I test it reliably?”
Compose makes building UI fun, but testing it is often underrated. Without solid tests, your beautiful UI can break in subtle ways that users notice long before you do. That’s where Espresso + Compose testing comes in – this article shows how to use them in real code, not just copy‑paste snippets.
Inspired by: “How to Test Jetpack Compose UIs Using Espresso”
Why Automated UI Testing Matters
-
Manual testing feels okay… until it doesn’t.
- Layout glitches slip through
- Recomposition bugs go unnoticed
- Edge cases eat your time
- Something that worked yesterday suddenly breaks
-
Automated testing
- Reduces manual QA effort
- Gives confidence for refactors
- Acts as living documentation
- Helps prevent regressions
Testing isn’t optional; it’s part of quality.
Espresso vs. Compose Test APIs
- Espresso was historically the go‑to UI test tool for Android Views.
- Compose introduced its own test APIs (
composeTestRule), but Espresso still works nicely when testing parts of your UI that compose with classic Views or interoperability components.
Setting Up the Test Rule
@get:Rule
val composeTestRule = createAndroidComposeRule()
This rule gives you an entry point to:
- Set content
- Interact with semantics
- Assert UI state
Place it in your instrumented test file (usually under androidTest/).
Finding Nodes
In Compose, elements are identified by semantics—roles, content descriptions, and custom test tags—rather than view IDs.
By Text
composeTestRule.onNodeWithText("Login").performClick()
By Test Tag
Add a tag in your composable:
Modifier.testTag("login_button")
Then locate it:
composeTestRule.onNodeWithTag("login_button").assertIsDisplayed()
Why use tags?
- Not visible to users
- Test‑only handles
- Keep test logic independent of design changes
Performing Actions
Compose test APIs let you simulate real user interactions:
composeTestRule.onNodeWithTag("email_field")
.performTextInput("dev@example.com")
Common actions:
performClick()performTextInput("hello")performScrollTo()
Making Assertions
composeTestRule.onNodeWithTag("submit_button")
.assertIsEnabled()
Useful assertions:
assertIsDisplayed()assertTextEquals("Welcome")assertIsEnabled()assertExists()
These ensure the UI is not only visible but also interactive as expected.
Handling Asynchronous UI
Wait for the UI to settle
composeTestRule.waitForIdle()
Wait for a specific condition
composeTestRule.waitUntil(timeoutMillis = 5_000) {
composeTestRule.onAllNodesWithTag("item")
.fetchSemanticsNodes()
.isNotEmpty()
}
This is handy when data loads asynchronously.
Mixing Espresso and Compose
If a screen mixes Compose and classic Views (e.g., a RecyclerView or WebView), you can combine both APIs:
// Espresso part
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.scrollToPosition(10))
// Back to Compose
composeTestRule.onNodeWithTag("footer").assertExists()
The ability to mix tools makes tests flexible on mixed projects.
Example: Testing a Login Screen
@Test
fun login_success_displaysWelcome() {
composeTestRule.onNodeWithTag("email_field")
.performTextInput("test@example.com")
composeTestRule.onNodeWithTag("password_field")
.performTextInput("password123")
composeTestRule.onNodeWithTag("login_button")
.performClick()
composeTestRule.onNodeWithText("Welcome Back!")
.assertIsDisplayed()
}
The test is clean, readable, and directly tied to user intent.
Benefits of Automated Compose Tests
| Manual Testing | Automated Compose Tests |
|---|---|
| Slow | Fast feedback |
| Hard to reproduce | Repeatable results |
| Missing coverage | Confidence during refactor |
| Inconsistent | Integration with CI/CD |
You’ll end up shipping safer, faster, and with fewer late‑night bug hunts.
Practical Reminders
- Use
testTag()liberally – makes targets predictable. - Prefer semantics over text lookups – UI text changes often.
- Group related actions in helper functions – reduces boilerplate.
- Run tests on multiple devices/emulators – behavior isn’t always consistent.
- Automate tests in CI/CD – make it part of your pipeline, not an afterthought.
Conclusion
Jetpack Compose makes UI elegant, and with the right tests, it becomes reliable too. By combining:
- Compose testing APIs
- Espresso for interop
- Clear semantics
- Smart assertions
you can confidently validate your UI behavior and avoid regressions. Testing makes Compose trustworthy.