Developing UI Tests for Android Apps (Espresso)
You wrote a login screen in Compose, set up Hilt, added Retrofit. You run the test — it fails. Why? Android UI tests are often flaky — the root cause is almost always ignoring IdlingResource. Developers insert Thread.sleep(2000) and hope it passes — that's a path to chaos. Without properly configured IdlingResource, stability rarely exceeds 60%. A test suite of 100+ tests requires 2–3 hours to run, half fail due to timing.
We offer turnkey Espresso UI test development. We design architecture with the Robot pattern, configure IdlingResource for all asynchronous operations, and integrate with Firebase Test Lab. Within 3–5 days you get a stable set of critical user flows (login, payment, core UX). One of our projects — an internet banking app with 1,200 tests — achieved 98.5% stability after IdlingResource tuning.
Google officially recommends using IdlingResource to synchronize tests with background tasks — it's the foundation of stable automated testing.
Fundamentals and Critical Nuances
The basic Espresso test structure: onView(matcher).perform(action).check(assertion).
@Test
fun loginWithValidCredentials_navigatesToHome() {
onView(withId(R.id.emailInput))
.perform(typeText("[email protected]"), closeSoftKeyboard())
onView(withId(R.id.passwordInput))
.perform(typeText("password123"), closeSoftKeyboard())
onView(withId(R.id.loginButton))
.perform(click())
onView(withId(R.id.homeTitle))
.check(matches(isDisplayed()))
}
The most common cause of flaky tests is IdlingResource. If the app performs an asynchronous request (Retrofit, Coroutine), Espresso is unaware and tries to find the next element before the operation completes. The solution — register an IdlingResource.
// For OkHttp/Retrofit use OkHttpIdlingResource
val idlingResource = OkHttpIdlingResource.create("okhttp", okHttpClient)
IdlingRegistry.getInstance().register(idlingResource)
// For coroutines use EspressoIdlingResource
IdlingRegistry.getInstance().register(EspressoIdlingResource.getIdlingResource())
Espresso runs 2–3 times faster than UIAutomator because it avoids sleep.
Why Do Espresso Tests Fail?
Besides IdlingResource, a common issue is race conditions when working with RecyclerView or multithreading. Use IdlingResource for asynchronous operations and CountingIdlingResource for background tasks. In our experience, after implementing IdlingResource, test stability jumps from 60% to 95%.
| Problem | Solution |
|---|---|
| Asynchronous requests (Retrofit, Coroutine) | OkHttpIdlingResource / EspressoIdlingResource |
| Image loading (Glide, Coil) | GlideIdlingResource |
| Animations (Transition, Lottie) | Disable animations in tests |
| RecyclerView with pagination | CountingIdlingResource |
Jetpack Compose UI Testing
If the project uses Compose, the Espresso approach is replaced by ComposeTestRule:
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
@Test
fun loginScreen_showsErrorOnInvalidEmail() {
composeTestRule.onNodeWithTag("emailInput")
.performTextInput("invalid-email")
composeTestRule.onNodeWithTag("loginButton")
.performClick()
composeTestRule.onNodeWithText("Invalid email format")
.assertIsDisplayed()
}
testTag in Compose — the equivalent of accessibilityIdentifier in iOS. We always assign tags to all testable elements using Modifier.testTag("loginButton").
How to Speed Up Test Writing?
We use the Robot pattern — the Compose equivalent of Page Object. It makes tests read like scenarios and simplifies refactoring. Comparison:
| Criterion | Robot pattern | Page Object |
|---|---|---|
| Syntax | Call chains (Builder) | Methods without returning object |
| Readability | High, resembles test case steps | Medium, requires additional structure |
| Reusability | Through robot composition | Through page class inheritance |
| Compose support | Native, no extra wrappers | Requires adaptation |
Robot pattern is a better fit for Compose projects. Example:
class LoginRobot(private val composeTestRule: AndroidComposeTestRule<*, *>) {
fun enterEmail(email: String) = apply {
composeTestRule.onNodeWithTag("emailInput").performTextInput(email)
}
fun enterPassword(password: String) = apply {
composeTestRule.onNodeWithTag("passwordInput").performTextInput(password)
}
fun clickLogin() = apply {
composeTestRule.onNodeWithTag("loginButton").performClick()
}
fun assertHomeVisible() {
composeTestRule.onNodeWithTag("homeScreen").assertIsDisplayed()
}
}
// Test reads like a scenario
@Test
fun validLogin_showsHome() {
LoginRobot(composeTestRule)
.enterEmail("[email protected]")
.enterPassword("pass123")
.clickLogin()
.assertHomeVisible()
}
Hilt and DI in Instrumented Tests
If the project uses Hilt, tests require HiltAndroidRule. The @BindValue annotation allows replacing a real repository with a fake without modifying production code — the test doesn't depend on the network or database.
How to Write a Stable Espresso Test in 5 Steps
- Define the scenario and identify asynchronous operations.
- Register IdlingResource for each operation.
- Use Robot pattern for structuring.
- Run the test locally, verify stability.
- Integrate with CI and Firebase Test Lab.
CI: Firebase Test Lab
A local emulator is sufficient for development, but before release — Firebase Test Lab with real devices. Example workflow (GitHub Actions):
- name: Run Espresso tests on Firebase Test Lab
run: |
gcloud firebase test android run \
--type instrumentation \
--app app/build/outputs/apk/debug/app-debug.apk \
--test app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
--device model=Pixel8,version=34 \
--device model=GalaxyS23,version=33
Results — Logcat, screenshots on failure, run video — are stored in Google Cloud Storage. CI setup is included in the scope of work.
What's Included in Espresso UI Test Development
- Designing test architecture (pattern selection, IdlingResource configuration)
- Writing critical user flows (login, payment, core UX) with Robot pattern
- CI/CD integration (GitHub Actions, Firebase Test Lab)
- Documentation for running and maintaining tests
- Team training (1–2 hour workshop)
We have developed UI tests for 15+ Android apps, average stability — 97%, largest project — 1,200 tests. Average run time for 100 tests — 15 minutes compared to 2 hours without optimization.
Timeline: from 3 to 10 days depending on app complexity. Pricing is individual. If you need help with implementation — contact us, we'll conduct an audit and propose a solution. Order development of a test suite — we guarantee >95% stability and one month of support.
Checklist for Stable UI Tests
- Always register IdlingResource for all background threads.
- Never use Thread.sleep() — use explicit waiting via IdlingResource.
- Ensure animations are disabled in the device test profile.
- Run tests on real devices via Firebase Test Lab.
- Clean state between tests (clearState in @Before).
- Use Robot pattern for readability and reusability.







