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 (
androidTestdirectory) - Dependencies:
androidTestImplementation 'androidx.test.espresso:espresso-core:<latest>'
androidTestImplementation 'androidx.test.ext:junit:<latest>'
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:<compose version>'
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
3. Set Up the Compose Test Rule
class ExampleEspressoTest {
@get:Rule
val composeTestRule = createComposeRule()
// ...
}
- 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())
}
-
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()
- 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.
- Don’t use
-
Missing Test Rules:
- Forgetting
@get:Rule val composeTestRule = createComposeRule()means your Compose tests won’t run properly.
- Forgetting
-
Relying on UI details:
- Targeting implementation details (like hardcoded text) makes tests break easily. Prefer unique
testTags or stable attributes.
- Targeting implementation details (like hardcoded text) makes tests break easily. Prefer unique
-
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
composeTestRuleand 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
testTagfor 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)