espresso-suite
Authors Espresso UI tests for Android - uses `onView(withId(...)).perform(...).check(matches(...))`, leans on Espresso's automatic synchronization (no `Thread.sleep`), wires `IdlingResource` for app-specific async, runs via `./gradlew connectedAndroidTest` and parses the JUnit XML output. Use when an Android app needs UI tests in Google's first-party framework.
espresso-suite
Overview
Per espresso-doc:
"Espresso is a framework for writing concise, beautiful, and reliable Android UI tests."
The canonical example:
@Test
fun greeterSaysHello() {
onView(withId(R.id.name_field)).perform(typeText("Steve"))
onView(withId(R.id.greet_button)).perform(click())
onView(withText("Hello Steve!")).check(matches(isDisplayed()))
}The shape: onView(<matcher>).perform(<action>).check(<matches>).
The framework auto-waits via "synchronization conditions" per espresso-doc:
"Each time a test invokes onView(), Espresso waits to perform the corresponding UI action or assertion until these synchronization conditions are met:
This eliminates the need for Thread.sleep for stock Android async work.
When to use
Step 1 - Add dependencies
// app/build.gradle
android {
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
androidTestImplementation 'androidx.test:rules:1.6.1'
}Per espresso-doc, Espresso ships as multiple packages:
| Package | Purpose |
|---|---|
espresso-core | Core View matchers, actions, assertions. |
espresso-web | WebView support. |
espresso-idling-resource | Synchronization for background jobs. |
espresso-contrib | DatePicker, RecyclerView, Drawer actions, accessibility checks. |
espresso-intents | Intent validation + stubbing. |
espresso-remote | Multi-process functionality. |
Step 2 - Author tests
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.*
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.rules.ActivityScenarioRule
@RunWith(AndroidJUnit4::class)
class CheckoutTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun applyPromoCode() {
onView(withId(R.id.promo_field)).perform(typeText("WELCOME10"), closeSoftKeyboard())
onView(withId(R.id.apply_button)).perform(click())
onView(withId(R.id.subtotal)).check(matches(withText("$22.49")))
}
}Step 3 - IdlingResource for app-specific async
When the app uses something Espresso doesn't auto-track (e.g. custom thread pool, RxJava chains), wire an IdlingResource:
class NetworkIdlingResource : IdlingResource {
@Volatile private var callback: IdlingResource.ResourceCallback? = null
@Volatile private var pendingRequests = 0
override fun getName() = "NetworkIdlingResource"
override fun isIdleNow() = pendingRequests == 0
override fun registerIdleTransitionCallback(cb: IdlingResource.ResourceCallback) {
callback = cb
}
fun increment() = synchronized(this) { pendingRequests++ }
fun decrement() = synchronized(this) {
pendingRequests--
if (pendingRequests == 0) callback?.onTransitionToIdle()
}
}
// Wire from production code at network call sites:
networkIdlingResource.increment()
api.fetchOrders().enqueue { _, _ -> networkIdlingResource.decrement() }
// Register in test setup:
@Before fun setup() { IdlingRegistry.getInstance().register(networkIdlingResource) }
@After fun teardown() { IdlingRegistry.getInstance().unregister(networkIdlingResource) }Without IdlingResource, Espresso assumes idle when its built-in trackers report idle - async work the framework can't see triggers flake.
Step 4 - Common matchers and actions
// Matchers
withId(R.id.button) // by ID
withText("Submit") // by displayed text
withContentDescription("Login") // by content description
hasSibling(withText("Email")) // structural relationship
isDisplayed() // currently visible
// Actions
click(), longClick()
typeText("..."), clearText()
scrollTo(), swipeLeft(), swipeUp()
pressBack()
// Assertions
matches(withText("Hello"))
matches(isDisplayed())
matches(isEnabled())
doesNotExist()Step 5 - RecyclerView interactions
// espresso-contrib provides RecyclerViewActions
onView(withId(R.id.cart_list))
.perform(RecyclerViewActions.actionOnItemAtPosition<MyViewHolder>(0, click()))
onView(withId(R.id.cart_list))
.perform(RecyclerViewActions.scrollTo<MyViewHolder>(hasDescendant(withText("BOOK-001"))))Step 6 - Run
# Unit + UI tests on connected device / emulator
./gradlew connectedAndroidTest
# With test orchestrator (each test in fresh process — preferred for isolation):
android {
defaultConfig {
testInstrumentationRunnerArguments clearPackageData: 'true'
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
}Step 7 - CI integration (Android emulator on GitHub Actions)
jobs:
ui-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
script: ./gradlew connectedAndroidTest
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: app/build/outputs/androidTest-resultsThe test results land in JUnit XML at app/build/outputs/androidTest-results/.../TEST-*.xml - consumable by junit-xml-analysis.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Thread.sleep for async waits | Espresso has built-in sync; sleeps mask race conditions. | Use IdlingResource for unsupported async (Step 3). |
withText("Submit") for buttons | Translation breaks the test. | withId(R.id.submit_button) or withContentDescription. |
Missing closeSoftKeyboard() after typing | Keyboard obscures next view; click() misses. | Chain closeSoftKeyboard() after typeText. |
Skipping IdlingResource for Retrofit / OkHttp | Network async invisible; flaky. | Wire IdlingResource (Step 3) OR use OkHttp's IdlingResource bridge. |
| One mega-test that asserts 10 things | One failure obscures others; debugging hard. | One assertion target per @Test. |
| Skipping test orchestrator | Tests share process; one test's state leaks. | Enable ANDROIDX_TEST_ORCHESTRATOR (Step 6). |