DEV Community

Andrea Sunny
Andrea Sunny

Posted on

How to Actually Test Jetpack Compose UIs with Espresso (Without Losing Your Mind)

If you’ve just moved to Jetpack Compose for your Android UI (like me!) but still want reliable UI testing, you might wonder: Do I use Espresso? Compose Test APIs? Both? How do they work together?

After digging into Compose UI testing and running into a few walls, here’s what actually worked - and the practical ways to catch sneaky UI bugs before your users do.

Why Compose UI Testing Matters (and How Espresso Fits In)

  • UI bugs often hide in plain sight. Maybe the animation works for you but breaks on another device. Maybe that new Compose screen looks fine - until a user can’t tap a button.
  • Automated UI tests help you catch these issues before they hit production.
  • With Jetpack Compose, Espresso isn’t replaced - it’s complemented. Compose brings its own testing APIs. But you can (and often should) use both.

Let’s walk through the setup, how it fits together, and real code examples.

How Compose UI Testing (with Espresso) Actually Works

Testing Compose UIs is a bit different than traditional Android Views, but the basics are still:

  • Set up your test class and environment
  • Render your Compose UI in a controlled test
  • Use Espresso/Compose test APIs to interact and make assertions

Setup: What You Need

  • Android project using Jetpack Compose
  • UI tests enabled (androidTest directory)
  • Dependencies:
  androidTestImplementation 'androidx.test.espresso:espresso-core:<latest>'
  androidTestImplementation 'androidx.test.ext:junit:<latest>'
  androidTestImplementation 'androidx.compose.ui:ui-test-junit4:<compose version>'
Enter fullscreen mode Exit fullscreen mode

1. Create Your UI Test Class

  • Create a new file in androidTest, e.g., ExampleEspressoTest.kt
  • This is where your UI test code lives

2. Import Required Testing Tools

import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.espresso.action.ViewActions.click
import org.junit.Rule
import org.junit.Test
Enter fullscreen mode Exit fullscreen mode

3. Set Up the Compose Test Rule

class ExampleEspressoTest {
    @get:Rule
    val composeTestRule = createComposeRule()
    // ...
}
Enter fullscreen mode Exit fullscreen mode
  • The rule launches your Compose UI in a test harness

4. Write a Test: Compose + Espresso in Action

Let’s write a minimal test that ensures a button is visible and clicks it:

@Test
fun testButtonVisibilityAndClick() {
    composeTestRule.setContent {
        Button(onClick = { /* no-op */ }) {
            Text("Click Me")
        }
    }
    // Check if button text exists with Espresso
    onView(withText("Click Me")).check(matches(isDisplayed()))
    // Click it as a user would
    onView(withText("Click Me")).perform(click())
}
Enter fullscreen mode Exit fullscreen mode
  • setContent { ... } renders your Compose UI just for the test
  • onView(withText()) lets Espresso find the button by text
  • isDisplayed() asserts it’s there
  • perform(click()) simulates a real tap

Prefer Compose Matchers for Compose UIs (When Possible)

You can interact directly with Compose:

composeTestRule.onNode(hasText("Click Me")).assertIsDisplayed().performClick()
Enter fullscreen mode Exit fullscreen mode
  • More reliable for complex UIs
  • Prefer onNode/Compose matchers unless you really need Espresso integration (e.g., hybrid View + Compose screens)

Common Compose UI Test Patterns

  • Matchers:
    • withText("text") (Espresso) | hasText("text") (Compose)
    • isDisplayed() (with both APIs)
  • Actions:
    • click() (Espresso/Compose)
  • Assertions:
    • .check(matches(isDisplayed())) (Espresso)
    • .assertIsDisplayed() (Compose)

Your test code should read like: Find this element, make sure I see it, then click - just like a user would.

Where You’ll Run Into Compose UI Testing

  • Migrating legacy UI tests: If you’re replacing classic Views with Compose, you’ll need to rethink your test logic.
  • Hybrid screens: Sometimes you have both Views and Compose in the same Activity. Both Espresso and Compose test APIs work together!
  • Custom UI components: Use these patterns to reliably assert and interact with custom composables.

What Developers Often Get Wrong

  • Mixing up Espresso and Compose APIs:
    • Don’t use onView() to interact with Compose-only components unless they’re displayed in hybrid layouts.
    • For pure Compose UIs, prefer composeTestRule.onNode(...) and related APIs.
  • Missing Test Rules:
    • Forgetting @get:Rule val composeTestRule = createComposeRule() means your Compose tests won’t run properly.
  • Relying on UI details:
    • Targeting implementation details (like hardcoded text) makes tests break easily. Prefer unique testTags or stable attributes.
  • Ignoring synchronization:
    • Compose and Espresso each handle UI thread sync differently; stick with one approach per test to avoid flaky results.

Useful Learning Resource

If you want a deeper technical guide - including more code snippets and explanations that clarified a lot for me - check out the original blog post I used as a reference: Testing Jetpack Compose Based Android UI Using Espresso

Key Takeaways

  • Compose UI testing is practical with the right setup - combine composeTestRule and Compose matchers for clarity.
  • Use Espresso only when mixing Compose and traditional Views, or if you need legacy integrations.
  • Make your tests read like user stories: find, check, act.
  • Don’t rely on fragile attributes - use testTag for robust selectors.
  • Learning resources (like the Appxiom blog) can save you hours of confusion.

Curious about why some UI tests fail on real devices or how to avoid flaky Compose UI tests? Share your experiences or questions in the comments!

Top comments (0)