Browse the Knowledge Hub211 resources
Espresso review: Android UI testing tutorial and pitfalls
Espresso is Google’s UI testing framework for Android apps, part of AndroidX Test. Tests are written in Kotlin or Java, compiled into a test APK and run by instrumentation in the same process as the app, which lets Espresso wait for the UI thread to go idle before every action. It is free and open source under Apache 2.0.
QAble has evaluated this tool and works alongside it. The factual sections are complete; our view is bounded to what we have actually seen rather than a long-term verdict.
- last reviewed
- 13 October 2026
- version covered
- Espresso 3.7 (AndroidX Test)
- licence
- Open source
At a glance
Espresso at a glance
- Vendor
- Category
- Mobile automation
- Licence
- Open source (Apache 2.0)
- Platforms
- Android
- Languages
- Kotlin, Java
- Integrates with
- Android Studio, Gradle, Gradle Managed Devices, Firebase Test Lab (deprecated)
- First released
- 2013
- Version reviewed
- Espresso 3.7 (AndroidX Test)
Verdict
Our verdict on Espresso
Strengths
- Waits for the main thread’s message queue and registered idling resources before every action, so most explicit waits disappear
- Runs in the app process, so tests can reach app classes, inject fakes and stub intents
- Kotlin or Java in the same repository, IDE and Gradle build as the app itself
- Free under Apache 2.0, maintained by Google as part of AndroidX Test
- Gradle Managed Devices define emulators in the build file for repeatable local and CI runs
Weaknesses
- Android only, and only the app under test: system dialogs and other apps need UI Automator
- Background threads, coroutines and network calls are not tracked without idling resources
- Matching Compose UI needs the Compose testing APIs, not onView
- Firebase Test Lab, the default hosted device farm, shuts down on 30 September 2027
Our take
Espresso is the strongest choice when an Android team owns its UI tests and keeps them in the app repository: tests run beside the app, the main-thread synchronisation removes most waiting code, and Android Studio runs them like unit tests. QAble has evaluated it rather than maintained a long-running suite on it, so treat this as a bounded take. Watch three things. Background work, such as coroutines or a network client, is invisible to Espresso until you expose it through idling resources or test dispatchers. Jetpack Compose screens need the Compose testing APIs, which can sit in the same test. And Firebase Test Lab, the usual hosted device farm for Espresso, is deprecated and shuts down on 30 September 2027, so new CI plans should point elsewhere.
We have evaluated this tool rather than maintained a long-running suite on it, so this is a bounded view rather than a long-term verdict. The rest of this page is factual.
How it works
How Espresso works
Instrumentation inside the app process
Espresso tests live in the module’s androidTest source set and build into a separate test APK. AndroidJUnitRunner, declared as the testInstrumentationRunner in Gradle, installs both APKs and runs the tests through Android instrumentation, in the same process as the app. That is the source of Espresso’s speed and of its limits. Tests can see the app’s classes, swap dependencies and check internal state, but they can only touch the app’s own windows. Permission dialogs, notifications, the launcher and other apps are outside its reach, which is what UI Automator, a separate AndroidX library that drives the device from outside the app, is for.
Waiting for the main thread to go idle
Every call to onView waits before acting or asserting until three conditions hold: the main thread’s message queue has nothing Espresso needs to process, no AsyncTask is running, and every registered idling resource reports idle. Layout passes, drawing and click handling all go through that queue, so a test that clicks a button and then checks the next screen needs no sleep. The same rule explains the most common timeout: an infinite animation or a constantly reposting handler keeps the queue busy, and Espresso waits for idleness that never comes until it fails.
Idling resources for work Espresso cannot see
Anything that runs off the main thread is invisible to Espresso by default, including coroutines on background dispatchers, executors, RxJava schedulers and network clients. An idling resource tells Espresso when such work is in flight. CountingIdlingResource is a counter, idle at zero; UriIdlingResource adds a quiet period between consecutive requests; IdlingThreadPoolExecutor wraps a thread pool. Resources are registered through IdlingRegistry, ideally in a @Before method and unregistered in @After. Google’s guidance is to wrap the app’s own async code rather than build the logic in the test, and to avoid Thread.sleep, retry loops and latches.
Matchers, actions and assertions
A test reads as onView(matcher).perform(action).check(assertion). Matchers are Hamcrest matchers such as withId, withText and isDisplayed, combined with allOf to narrow a match. A matcher must identify exactly one view in the hierarchy: zero matches fail with NoMatchingViewException and several with AmbiguousViewMatcherException, which is strict but makes tests precise. Lists need their own entry points, onData for AdapterView-based lists and RecyclerViewActions from espresso-contrib for RecyclerView, because off-screen rows do not exist as views until they are scrolled into place.
Strengths
What it does well
Synchronisation without waiting code
Because Espresso checks the main thread and registered idling resources before every step, a typical test is a list of actions and assertions with no waits at all. That is the main reason Espresso suites are faster and less flaky than equivalent suites driven from outside the app, provided background work is exposed correctly.
Hermetic tests with Espresso-Intents
espresso-intents records intents the app sends and can stub the responses. intended verifies that the app tried to open the dialler or the camera; intending returns a canned ActivityResult so a test never leaves the app. That keeps flows involving contacts, cameras or payment apps deterministic and fast.
WebView, lists and pickers
espresso-web drives web content inside a WebView with web-style locators. espresso-contrib adds actions for RecyclerView, DatePicker and navigation drawers, plus accessibility checks. These cover the screens where plain onView struggles, without switching tools mid-test.
Isolation with Android Test Orchestrator
Android Test Orchestrator runs each test in its own instrumentation invocation, and with clearPackageData it clears the app’s data between tests. A crash then fails one test instead of the whole run, and shared state cannot leak from test to test, at the cost of a slower run.
Emulators defined in the build
Gradle Managed Devices declare emulator profiles, API levels and system images in the build file. Gradle downloads the image, creates the device, runs the tests and shuts it down, and device groups run the same tests across several profiles in parallel. Automated Test Device images strip preinstalled apps and services for faster CI runs.
Compose and View tests side by side
In an app that mixes Views and Jetpack Compose, one test can use Espresso’s onView for Views and a ComposeTestRule for composables, with no special setup. That makes Espresso suites viable through a gradual migration to Compose rather than forcing a rewrite.
Pricing
Espresso pricing and licensing
Espresso and the rest of AndroidX Test are free and open source under the Apache 2.0 licence. There is no paid tier, account or licence key. The costs are devices and CI time: emulators need hardware acceleration and memory, physical devices need a lab or a cloud provider, and Google’s own hosted option, Firebase Test Lab, is deprecated, with full shutdown on 30 September 2027 and a migration path to Google Cloud’s Developer Device Platform.
- Licence: Apache 2.0, stated in the android/android-test repository that holds Espresso and AndroidX Test.
- Local runs: free on any emulator or USB-connected device; Gradle Managed Devices need nothing beyond the Android SDK.
- Firebase Test Lab: list prices on its pricing page on 13 October 2026 were $1 per virtual device hour and $5 per physical device hour on the Blaze plan, after the Spark plan’s 10 virtual and 5 physical test runs a day.
- Firebase Test Lab is deprecated and shuts down on 30 September 2027; Google points users to the Developer Device Platform on Google Cloud.
- Hidden cost: idling resources and test hooks live in the app code, so developers maintain them alongside features.
Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.
Getting started
Espresso tutorial: getting started
Setup, step by step
- Open the app module’s build.gradle.kts and set testInstrumentationRunner to androidx.test.runner.AndroidJUnitRunner.
- Add the Espresso, AndroidX JUnit and runner dependencies to androidTestImplementation.
- Turn off window, transition and animator animations on the test device in Developer options.
- Create a test class under src/androidTest with an ActivityScenarioRule for the screen you want to start on.
- Run it from the gutter icon in Android Studio, or with ./gradlew connectedAndroidTest against a running emulator.
Install
// app/build.gradle.kts
android {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test:runner:1.7.0")
}
# then, with an emulator running
./gradlew connectedAndroidTestA first example
// app/src/androidTest/java/com/example/app/LoginTest.kt
package com.example.app
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class LoginTest {
// Launches LoginActivity before each test and closes it afterwards
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@Test
fun validLoginShowsWelcome() {
onView(withId(R.id.email)).perform(typeText("[email protected]"), closeSoftKeyboard())
onView(withId(R.id.password)).perform(typeText("correct-horse"), closeSoftKeyboard())
onView(withId(R.id.sign_in)).perform(click())
// No sleep: Espresso waits for the main thread to go idle first
onView(withText("Welcome back")).check(matches(isDisplayed()))
}
}What trips people up first
A test that passes on one run and times out on the next, usually because the app does something Espresso cannot see. Animations left on make views move under the click, so switch off the three animation scales on the device, or set animationsDisabled = true under testOptions for command-line runs. A sign-in that calls the network on a coroutine returns control to the main thread at once, so Espresso checks for the welcome screen before the response arrives. The fix is an idling resource around that work, or a test dispatcher injected for tests, never a Thread.sleep.
In practice
Using Espresso in practice
Structure: test ids, robots and fakes
Keep UI tests in the androidTest source set of the module that owns the screen, and give every view a test reaches a stable resource id rather than matching on text that translators will change. A common way to keep tests readable is a robot per screen: a small class that wraps the onView calls for that screen and exposes intent-level methods, so the test says what the user does and the robot knows how. Because tests run in the app process, swap slow or flaky dependencies such as the network layer for fakes through your dependency injection setup, and keep a separate, smaller set of tests against a real backend.
// app/src/androidTest/java/com/example/app/robots/LoginRobot.kt
// Imports as in the first test, plus ViewActions.replaceText and Matchers.allOf
class LoginRobot {
fun typeEmail(email: String) = apply {
onView(withId(R.id.email)).perform(replaceText(email), closeSoftKeyboard())
}
fun typePassword(password: String) = apply {
onView(withId(R.id.password)).perform(replaceText(password), closeSoftKeyboard())
}
fun submit() = apply { onView(withId(R.id.sign_in)).perform(click()) }
fun assertError(text: String) = apply {
onView(allOf(withId(R.id.error), withText(text))).check(matches(isDisplayed()))
}
}
// In a test
@Test
fun wrongPasswordShowsError() {
LoginRobot()
.typeEmail("[email protected]")
.typePassword("wrong")
.submit()
.assertError("Incorrect password")
}Exposing background work with an idling resource
The pattern Google recommends is to keep the idling logic in the app and only register it in tests. Add espresso-idling-resource as an implementation dependency, increment a CountingIdlingResource when background work starts and decrement it when it finishes, then register it with IdlingRegistry in @Before and unregister it in @After. Call decrement in a finally block, or a failed request leaves the counter above zero and every later test waits until it times out. Many teams with coroutines prefer to inject a test dispatcher instead, which runs the work in a way the test controls; either is better than sleeping.
// app/src/main/java/com/example/app/NetworkIdling.kt (app code)
// Needs implementation("androidx.test.espresso:espresso-idling-resource:3.7.0")
import androidx.test.espresso.idling.CountingIdlingResource
object NetworkIdling {
val resource = CountingIdlingResource("network")
suspend fun <T> track(block: suspend () -> T): T {
resource.increment()
try {
return block()
} finally {
resource.decrement() // always, or later tests hang
}
}
}
// app/src/androidTest/.../OrdersTest.kt
@Before
fun registerIdling() {
IdlingRegistry.getInstance().register(NetworkIdling.resource)
}
@After
fun unregisterIdling() {
IdlingRegistry.getInstance().unregister(NetworkIdling.resource)
}Stubbing other apps with Espresso-Intents
A flow that picks a contact, takes a photo or opens a payment app leaves your process, and Espresso cannot follow. Stub it instead. Add espresso-intents and declare an IntentsRule, the current rule for initialising and releasing Espresso-Intents; the older IntentsTestRule is deprecated. intending matches outgoing intents and answers them with a canned ActivityResult, so the app receives a result without anything launching. intended verifies after the fact that the app sent the intent you expected. Together they test your side of the contract hermetically, and a separate UI Automator test can cover the real cross-app journey if it matters.
// Needs androidTestImplementation("androidx.test.espresso:espresso-intents:3.7.0")
// Imports: Intents.intended, Intents.intending, IntentMatchers.toPackage,
// androidx.test.espresso.intent.rule.IntentsRule, plus the usual Espresso ones
@RunWith(AndroidJUnit4::class)
class PickContactTest {
@get:Rule(order = 0) val intentsRule = IntentsRule()
@get:Rule(order = 1) val activityRule = ActivityScenarioRule(InviteActivity::class.java)
@Test
fun pickedContactFillsThePhoneField() {
val data = Intent().putExtra("phone", "123-345-6789")
// Answer the contacts picker without launching it
intending(toPackage("com.android.contacts"))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, data))
onView(withId(R.id.pick_contact)).perform(click())
intended(toPackage("com.android.contacts"))
onView(withId(R.id.phone)).check(matches(withText("123-345-6789")))
}
}Running in CI on managed devices
Gradle Managed Devices make CI emulators part of the build: declare a device profile, API level and system image source under testOptions, and Gradle provisions the emulator, runs the tests and tears it down with a task named after the device and build variant. On GitHub-hosted Linux runners enable KVM for acceleration, and pass -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect because those runners have no hardware rendering. Upload the reports folder when a run fails. Automated Test Device images, aosp-atd or google-atd, start faster, but Google documents them for API level 30 only and without hardware-rendered screenshots.
# .github/workflows/android-ui.yml
name: android-ui
on: [push, pull_request]
jobs:
espresso:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v6
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Espresso on a managed emulator
run: ./gradlew pixel6api34DebugAndroidTest -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect
- uses: actions/upload-artifact@v7
if: failure()
with:
name: android-test-reports
path: app/build/reports/androidTestsIsolation, sharding and test size
As a suite grows, three controls keep it fast and trustworthy. Android Test Orchestrator, enabled with execution set to ANDROIDX_TEST_ORCHESTRATOR in testOptions, the orchestrator artifact as androidTestUtil and the clearPackageData runner argument, gives every test a fresh process and fresh app data. Sharding splits the suite: AndroidJUnitRunner accepts numShards and shardIndex arguments for spreading tests across devices, and Gradle Managed Devices can provision several emulators per profile through the numManagedDeviceShards property. Size annotations, @SmallTest, @MediumTest and @LargeTest, let CI run a quick subset on every pull request with -Pandroid.testInstrumentationRunnerArguments.size=small and the full set nightly.
// app/build.gradle.kts (excerpt)
android {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// Orchestrator runs pm clear after each test
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}
testOptions {
execution = "ANDROIDX_TEST_ORCHESTRATOR"
animationsDisabled = true // command-line runs only
managedDevices {
localDevices {
create("pixel6api34") {
device = "Pixel 6"
apiLevel = 34
systemImageSource = "aosp"
}
}
}
}
}
dependencies {
androidTestImplementation("androidx.test:runner:1.7.0")
androidTestUtil("androidx.test:orchestrator:1.6.1")
}
// Run: ./gradlew pixel6api34DebugAndroidTestPitfalls
Common pitfalls
Leaving system animations on
Why it happens: Espresso does not wait for animations driven outside the message queue, so a view can still be moving when the click lands, and results depend on device speed.
The fix: Turn off window, transition and animator scales on every test device, and set animationsDisabled = true in testOptions for command-line runs; it has no effect on runs started from Android Studio.
Thread.sleep to wait for data
Why it happens: Espresso cannot see coroutines, executors or network calls, so a sleep seems to fix the race. It is too short on a slow emulator and wasted time everywhere else.
The fix: Wrap the background work in a CountingIdlingResource registered through IdlingRegistry, or inject a test dispatcher. Google’s idling resource guide names sleeps, retry loops and latches as workarounds to avoid.
Copying tests that use ActivityTestRule or android.support.test
Why it happens: Older tutorials predate AndroidX Test. The support test libraries were replaced by androidx.test, and ActivityTestRule and IntentsTestRule are deprecated.
The fix: Use ActivityScenarioRule or ActivityScenario from androidx.test.ext.junit and androidx.test.core, IntentsRule for intents, and the androidx.test package names throughout.
AmbiguousViewMatcherException on lists and repeated layouts
Why it happens: A matcher such as withId(R.id.title) matches every row that reuses the layout, and Espresso refuses to guess.
The fix: Use RecyclerViewActions or onData to address one row, or narrow the matcher with allOf, withParent or hasDescendant until it identifies a single view.
A permission dialog blocks the test
Why it happens: Runtime permission prompts belong to the system, outside the app’s windows, so Espresso can neither see nor tap them.
The fix: Grant permissions up front with GrantPermissionRule for tests that assume access, and cover the prompt itself with a UI Automator test.
onView cannot find a Jetpack Compose element
Why it happens: Composables are not Android Views, so ViewMatchers do not match them; they are found through the Compose semantics tree.
The fix: Add a ComposeTestRule and use onNodeWithText or onNodeWithTag for Compose UI, keeping onView for Views in the same test where screens mix both.
Compared
Espresso alternatives compared
| Tool | Who usually writes tests | Test languages | Process model | Synchronisation | Platforms |
|---|---|---|---|---|---|
| Espresso | Android developers | Kotlin, Java | Inside the app process | Automatic main-thread idle checks | Android only |
| Appium | QA engineers and SDETs | Java, Python, JS, C#, Ruby | External server over WebDriver | Explicit waits in test code | Android, iOS, desktop |
| Maestro | Testers, developers and agents | YAML, optional JavaScript | Black box via accessibility layer | Built-in settle waits and retries | Android, iOS simulators, web |
| Detox | React Native developers | JavaScript, TypeScript | Grey box, instrumented app build | Monitors the app’s async work | React Native on Android, iOS |
FAQ
Frequently asked questions
What are the best Espresso alternatives?
The alternatives most often weighed against Espresso are Appium, Maestro and Detox. The comparison on this page sets them side by side on who usually writes tests, test languages, process model, synchronisation and platforms, and the verdict above says where Espresso is the wrong choice and which of them to pick instead.
Is Espresso free?
Yes. Espresso is part of AndroidX Test, which Google publishes as open source under the Apache 2.0 licence, and it runs on any emulator or device with no account or licence key. Costs come from devices and CI time, and from any hosted device service you choose. Firebase Test Lab, the long-standing option, is deprecated and shuts down on 30 September 2027.
Should I use Espresso or Appium?
Choose Espresso when Android developers own the tests, write Kotlin or Java and want fast, synchronised tests in the app repository. Choose Appium when a separate QA team covers Android and iOS in one language, tests the build from outside or needs one suite for both platforms. The two can coexist: Espresso for developer feedback on each change, Appium for cross-platform journeys.
What is the difference between Espresso and UI Automator?
Espresso runs inside your app’s process and synchronises with its main thread, so it is the precise tool for your own screens. UI Automator drives the device from outside the app, so it can handle system dialogs, notifications, other apps and minified release builds, and Google points to it for writing Macrobenchmark tests. Many suites use Espresso for app screens and UI Automator for the few steps outside the app.
Can Espresso test Jetpack Compose?
Not with onView, because composables are not Views. Compose has its own testing APIs, created through a ComposeTestRule, with finders such as onNodeWithText and onNodeWithTag. In hybrid apps both can be used in the same test with no special setup: Espresso for Views, the Compose rule for composables. For a pure Compose app, the Compose testing APIs do the work Espresso does for Views.
Which version of Espresso should I use?
Espresso 3.7.0, released on 30 July 2025 alongside AndroidX Test runner and rules 1.7.0 and ext.junit 1.3.0, was the latest stable release on the review date. It raised the minimum SDK to API 21. The Espresso setup page still shows 3.6.1 in its examples, so take version numbers from the AndroidX Test release notes rather than the setup guide.
How do I run Espresso tests in CI?
Use Gradle Managed Devices so the emulator is declared in the build file, then run the device task, such as pixel6api34DebugAndroidTest, on a Linux runner with KVM enabled. On runners without hardware rendering, including GitHub-hosted ones, add the swiftshader_indirect GPU property. Alternatively start an emulator with a CI action and run connectedAndroidTest. Upload the reports folder on failure.
Is Espresso still worth learning in 2026?
Yes for Android engineers working on View-based or mixed apps, which still describes many production apps, and its synchronisation model is the clearest introduction to why in-process UI tests are fast and stable. For new Compose-only screens, learn the Compose testing APIs alongside it, since they follow the same rule-and-finder style and can share a test.
Alternatives
Espresso alternatives worth weighing
Appium
Open-source automation server that drives iOS, Android, tvOS, Windows and macOS apps through the W3C WebDriver protocol, using separately installed platform drivers.
Detox
Grey-box end-to-end framework for React Native, synchronising with the app rather than polling.
Maestro
Declarative mobile UI testing with flows defined in YAML and built-in tolerance for flakiness.
Resources
Where to go next
Keep going
Prepare, or get it done
Picking the wrong tool costs more than picking late.
QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.