DEV Community

Cover image for How to Confidently Test Jetpack Compose UI with Espresso
Andrea Sunny
Andrea Sunny

Posted on

How to Confidently Test Jetpack Compose UI with Espresso

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 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 - and this article helps you actually understand how to use them in real code, not just copy/paste snippets.

Inspired by: How to Test Jetpack Compose UIs Using Espresso

Why UI Testing Matters (and What Usually Fails)

Let’s be honest:

Manual testing feels okay until it doesn’t.

You may catch obvious crashes, but:

  • Layout glitches slip through
  • Recomposition bugs go unnoticed
  • Edge cases eat your time
  • Something that worked yesterday suddenly breaks

Especially with Compose’s dynamic UI and state system, UI tests catch problems early.

Automated testing:

  • Reduces manual QA effort
  • Gives confidence for refactors
  • Acts as living documentation
  • Helps prevent regressions

So yes - testing isn’t optional. It’s part of quality.

Espresso Meets Jetpack Compose - The Basics

Espresso was historically the go-to UI test tool for Android Views.

Compose introduced its own test APIs (composeTestRule), but Espresso still plays nicely when testing parts of your UI that compose with classic Views or interoperability components.

Before you write a test, set up your test rule:

@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
Enter fullscreen mode Exit fullscreen mode

This gives you an entry point to:

  • Set content
  • Interact with semantics
  • Assert UI state

Plug this into your instrumented test file (usually under androidTest/).

Finding UI Elements

In Compose, elements are identified not by IDs, but by semantics - roles, content descriptions, and custom test tags.

For example:

composeTestRule.onNodeWithText("Login").performClick()
Enter fullscreen mode Exit fullscreen mode

This finds a node displaying text and clicks it. Simple.

But often you’ll want a custom tag:

Modifier.testTag("login_button")
Enter fullscreen mode Exit fullscreen mode

Then find it:

composeTestRule.onNodeWithTag("login_button").assertIsDisplayed()
Enter fullscreen mode Exit fullscreen mode

This works great because:

  • Tags aren’t visible to users
  • They’re test-only handles
  • They keep your test logic independent of design changes

Interacting with UI

Testing isn’t just about looking - it’s about doing.

Compose test APIs let you:

  • performClick()
  • performTextInput("hello")
  • performScrollTo()

For example:

composeTestRule.onNodeWithTag("email_field")
    .performTextInput("dev@example.com")
Enter fullscreen mode Exit fullscreen mode

This simulates the user typing into the field - a real interaction, not just a state change.

Asserting UI Behavior

Assertions tell you what should have happened.

Useful assertions include:

assertIsDisplayed()
assertTextEquals("Welcome")
assertIsEnabled()
Enter fullscreen mode Exit fullscreen mode

For example:

composeTestRule.onNodeWithTag("submit_button")
    .assertIsEnabled()

Enter fullscreen mode Exit fullscreen mode

This makes sure your UI isn’t only visible, it’s interactive as expected.

Dealing With Asynchronous UI

UI often updates asynchronously - for example, after network calls or state updates.

Compose test rules let you wait for the UI to settle:

composeTestRule.waitForIdle()
Enter fullscreen mode Exit fullscreen mode

This makes sure no pending changes are in flight before you assert.

Sometimes you need to wait for a condition:

composeTestRule.waitUntil(timeoutMillis = 5_000) {
    composeTestRule.onAllNodesWithTag("item").fetchSemanticsNodes().isNotEmpty()
}
Enter fullscreen mode Exit fullscreen mode

This waits up to 5 seconds for something to appear - handy with asynchronous data.

Espresso + Compose - Real Interop

If your screen mixes Compose and classic Views (e.g., a RecyclerView or WebView), combine Espresso and Compose APIs:

onView(withId(R.id.recycler_view))
    .perform(RecyclerViewActions.scrollToPosition<MyViewHolder>(10))
Enter fullscreen mode Exit fullscreen mode

And then back to Compose:

composeTestRule.onNodeWithTag("footer").assertExists()
Enter fullscreen mode Exit fullscreen mode

The ability to mix both tools makes tests flexible on mixed projects.

Jetpack Compose UI components highlighted for automated testing

A Real Example Workflow

Let’s say you want to test a login screen:

  1. Launch the UI
  2. Type valid credentials
  3. Click the login button
  4. Check for the welcome message

In code:

@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()
}
Enter fullscreen mode Exit fullscreen mode

Clean. Readable. And directly connected to user intent.

Why This Is Better Than Manual QA

Manual testing is:

  • Slow
  • Hard to reproduce
  • Missing coverage
  • Inconsistent

Automated Compose tests give:

  • Repeatable results
  • Confidence during refactor
  • Fast feedback
  • Integration with CI/CD

You’ll end up shipping safer, faster, and with fewer late-night bug hunts.

Final Tips for Daily Practice

Here are a few practical reminders:

  • Use testTag() liberally - it makes targets predictable.
  • Prefer semantics over text lookups - UI text changes often.
  • Group related actions in helper functions - reduce 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.

Wrapping Up

Jetpack Compose makes UI elegant - and with the right tests, reliable too.

By combining:

  • Compose testing APIs
  • Espresso for interop
  • Clear semantics
  • Smart assertions

…you can confidently validate your UI behavior and avoid regressions.

Compose is powerful. Testing makes it trustworthy.

Top comments (0)