← all articles

Espresso vs UI Automator: which Android test framework to use

Google ships two official ways to automate an Android UI test, and they don’t do the same job. Espresso talks to your app’s views directly, from inside the same process your app runs in. UI Automator talks to whatever’s drawn on screen, through Android’s accessibility tree, whether that’s your app, a system permission dialog, or the home screen launcher. Mix up which one you need and you’ll lose an afternoon wondering why Espresso can’t see a system alert it was never built to see in the first place.

If you’re testing your own app’s screens in CI and want fast, deterministic assertions tied to view IDs, Espresso wins that job outright. If the test needs to leave your app, tap “allow” on a runtime permission prompt, pull down the notification shade, or hand off to another installed app and come back, Espresso can’t do any of that alone, and UI Automator (often paired with Espresso in the same test) is the answer.

Most real Android test suites end up using both. This isn’t really a pick-one comparison so much as a division of labour between two tools built by the same team for two different jobs. The rest of this covers where that line actually sits, with real package names and gradle coordinates you can check against the docs yourself.

TL;DR comparison table

Espresso UI Automator
pricing free, open source, part of AndroidX Test (androidx.test.espresso:espresso-core) free, open source, part of AndroidX Test (androidx.test.uiautomator:uiautomator)
model in-process, white-box, sees your app’s actual View objects out-of-process, black-box, sees only the accessibility node tree
synchronisation automatic, blocks until the main thread and message queue are idle manual, you call device.wait() / waitForIdle() yourself
cross-app reach none, confined to your own app’s process full device, any app, system UI, notifications, permission dialogs
support Google, via AndroidX Test release notes and Stack Overflow’s espresso tag Google, same release train, Stack Overflow’s android-uiautomator tag
target user app engineers writing CI tests against their own screens QA/automation engineers testing across app or system boundaries

Espresso at a glance

Espresso is Google’s in-process UI testing framework for Android, distributed as androidx.test.espresso:espresso-core and documented at developer.android.com/training/testing/espresso. You add it as a testImplementation (or androidTestImplementation, since it runs on-device via instrumentation) alongside AndroidJUnitRunner, then write tests like onView(withId(R.id.loginButton)).perform(click()).

The thing that makes Espresso pleasant to work with is synchronisation. Before it performs the next action or assertion, Espresso waits for the main thread to be idle and the message queue to be empty. That kills most of the Thread.sleep(2000) hacks people reach for in flaky test suites. It extends that idling to the default AsyncTask thread pool too, and you can register your own IdlingResource for background work Espresso doesn’t already track (Retrofit calls, coroutine work, custom executors) — the mechanism is documented at developer.android.com/training/testing/espresso/idling-resource.

Espresso has two extensions worth knowing: Espresso-Intents, which lets you stub or verify outgoing Intents without actually launching the other app, and Espresso-Web, for asserting on WebView content via a JavaScript bridge. Neither replaces UI Automator, they just extend what Espresso can check without leaving your process.

The catch is right there in “in-process.” Espresso can only see views your own app draws. It can’t tap a system permission dialog, the notification shade, or another app’s screen, because none of that lives in your app’s process or view hierarchy. And if your screens are built with Jetpack Compose rather than the classic View system, Espresso isn’t really the first tool to reach for, Compose ships its own testing library (androidx.compose.ui:ui-test-junit4) built on the same synchronisation idea but aimed at the Compose tree. I haven’t run a large Espresso suite against a Compose-only screen and won’t pretend I have an opinion worth trusting there.

UI Automator at a glance

UI Automator is Google’s black-box automation framework, currently shipped as androidx.test.uiautomator:uiautomator and documented at developer.android.com/training/testing/other-components/ui-automator. It’s older than the AndroidX packaging suggests, the underlying UiAutomation class was added in API level 18 (Android 4.3, 2013), and it originally shipped as a standalone SDK tool with its own uiautomatorviewer inspector before Google folded it into AndroidX Test alongside Espresso.

The core objects are UiDevice (the whole screen and device), UiObject2 and BySelector (the modern fluent selector API that replaced the older UiObject/UiSelector classes). A selector can match on text, content description, resource-id, or class name, whatever shows up in the accessibility tree, which you can inspect live with Android Studio’s Layout Inspector or the older uiautomatorviewer. Because it works purely off what’s rendered and labelled on screen, UI Automator doesn’t care which app owns that screen. It’ll happily click through your app, into a system dialog, into a different installed app, and back.

That reach comes at a cost: no built-in synchronisation. Since UI Automator isn’t running inside the target app’s process, it has no idea when that app’s main thread goes idle. Every wait is explicit, device.wait(Until.hasObject(By.text("Allow")), 5000) or similar, and skipping that step is the single most common cause of flaky UiAutomator tests.

Worth flagging since the name is genuinely confusing: “UiAutomator2” is not this framework, it’s Appium’s Android driver, which wraps this same accessibility mechanism to expose it over Appium’s WebDriver protocol for cross-language remote control. If you’re deciding between writing raw AndroidX UI Automator tests versus going through Appium’s UiAutomator2 driver, I’ve laid out that tradeoff separately in uiautomator2 vs appium for android automation.

Head-to-head

setup

Both frameworks run through the same instrumentation mechanism: an AndroidJUnitRunner declared as testInstrumentationRunner in your module’s build.gradle, executed with ./gradlew connectedAndroidTest against an emulator or a physical device with USB or wireless debugging enabled. You can add androidx.test.espresso:espresso-core and androidx.test.uiautomator:uiautomator to the same androidTestImplementation block and call into both from the same test class, they don’t conflict.

Getting a device to actually accept that instrumentation call is its own small hurdle if you haven’t set it up before. If the machine running your test suite has never talked to that device, you’ll need to authorise it first, which I cover in adb authorization and RSA keys explained. And if you’re weighing emulator versus real hardware for the run itself, that’s a separate decision with its own tradeoffs I already wrote up in emulator vs real device testing.

in-process vs black-box

This is the real dividing line, everything else in this article follows from it. Espresso has a reference to your actual View objects, so onView(withId(...)) is checking real object state, not a rendered snapshot. UI Automator only has whatever the Android accessibility service exposes as a node tree, it has no idea your app exists as code, it just sees pixels-with-labels. Practical consequence: a custom-drawn view (raw Canvas, a game surface, a chart with no content description) is functionally invisible to UI Automator, and only visible to Espresso if you’re matching on the View object itself rather than its accessibility label. Neither tool can assert on genuinely unlabelled custom drawing, they just fail in different ways.

synchronisation

Espresso’s auto-sync means most tests need zero explicit waits. UI Automator’s manual model means every test needs them, because it has no window into a foreign app’s thread state. This is the part people underestimate when they first reach for UI Automator: it’s not that it’s a worse API, it’s that you’re now responsible for the timing logic Espresso used to handle for you.

cross-app reach

The clearest example is the runtime permission dialog, which has been system UI since API 23 (Android 6.0). It’s drawn outside your app’s process, so Espresso literally cannot see or tap it, there’s no view of yours to match against. You’ve got two honest options: skip the dialog entirely with GrantPermissionRule, which grants the permission via a shell command before the test runs (worth knowing that pm grant is the same underlying mechanism, and I go through it and its siblings in adb pm and am commands to manage apps), or actually drive the real dialog with UI Automator if you specifically need to prove that flow works end to end.

My honest take here: don’t bother scripting UiAutomator selectors against another app’s system dialog unless you genuinely need to verify that exact dialog. OEM skins (Samsung’s One UI, Xiaomi’s MIUI, whatever a given device ships) rewrite the accessibility tree for their own permission UI, so a selector that matches stock Android’s “Allow” button can silently fail on a Samsung device with different button text or layout. Grant the permission and test your own screen’s behaviour after it, most of the time that’s what you actually care about.

Beyond permissions, this same out-of-process reach is what you need for testing a share sheet, a return from another installed app, or the notification shade. If your CI needs to run this against a device you don’t physically own, say a real Singapore-located handset for geo-specific behaviour, the access model matters more than the test framework does: both Espresso and UI Automator ride on the same instrumentation call, and that call needs adb reaching the device. cloudf.one rents real Android phones on dedicated hardware in Singapore, each with a persistent Singapore mobile IP, but there’s no self-service adb, adb and Appium reach a rented phone only through a WireGuard tunnel they set up on request by emailing [email protected]. Worth knowing before you assume you can just point connectedAndroidTest at a rented device the way you would a local one.

combining both in one suite

Because both frameworks share the same instrumentation runner, you can call one from inside a test written primarily for the other. A test that clicks a button with Espresso, waits for the resulting system permission dialog, taps it with UI Automator, then asserts on your app’s post-permission state back in Espresso, all in one method:

@Test
fun grantsCameraPermissionAndShowsPreview() {
    onView(withId(R.id.captureButton)).perform(click())

    val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    device.wait(Until.findObject(By.text("Allow")), 5000)?.click()

    onView(withId(R.id.cameraPreview)).check(matches(isDisplayed()))
}

That’s the pattern most production suites actually converge on. Not “Espresso or UI Automator” but Espresso for the parts that stay inside your app, UI Automator for the two or three moments a flow steps outside it.

limits

Espresso’s limits: confined to your own process, weak fit for Compose-only screens (use ui-test-junit4 instead), and blind to any view with no accessible way to match it if you’re going through matchers rather than direct object references. UI Automator’s limits: no automatic synchronisation so every test needs explicit waits or it’ll flake, blind to unlabelled custom-drawn content just like Espresso is, generally slower per-action than Espresso because every interaction round-trips through the accessibility service, and selectors written against system UI or another app’s UI are fragile across OEM skins and Android versions in a way selectors against your own app never are.

Use-case verdicts

  • CI regression suite covering your own app’s screens: Espresso. Automatic sync means fewer flaky waits, and it’s the faster of the two for pure in-app assertions.
  • Runtime permission or first-run onboarding flow: UI Automator if you need to prove the actual system dialog works, otherwise Espresso plus GrantPermissionRule if you just need the permission granted so you can test what comes after.
  • Cross-app handoff, share sheets, returning from another installed app, notification shade interaction: UI Automator, this is squarely what it’s for and Espresso structurally can’t reach it.
  • An app built entirely in Jetpack Compose: neither, reach for androidx.compose.ui:ui-test-junit4 first, it’s built on the same idling model as Espresso but targets the Compose tree directly.

Who should pick Espresso

If you’re an app engineer writing instrumented tests against screens your own team built, and you want those tests to run fast and stay green without babysitting timing, Espresso is the default. It’s what most Android CI pipelines are built around, it’s what most existing test code in a typical Android repo already uses, and you won’t touch UI Automator until a test needs to step outside your app.

Who should pick UI Automator

If you’re a QA or automation engineer whose job includes flows that cross app boundaries, system dialogs, notifications, the launcher, or another installed app, you need UI Automator, Espresso structurally cannot get there. It’s also the better starting point if you’re automating something adjacent to test authoring entirely, like scripted device setup, where you’re really just issuing taps and swipes rather than asserting on view state. If that’s closer to what you actually need, plain adb shell input tap, swipe, text, and keyevent commands might be simpler than standing up a full instrumentation test for it.

Verdict overall

These aren’t really competing products, they’re both maintained by Google, both free, both distributed under AndroidX Test, and built to be used together rather than instead of each other. If you had to bet on one, Espresso covers the vast majority of what a typical Android test suite needs, since most of a test suite’s job is asserting on your own screens. UI Automator earns its place for the smaller number of moments a test has to leave your app’s process, permission dialogs being the one nearly every app runs into eventually.

Start with Espresso. Add UI Automator to the same test class the first time a test needs to touch something outside your app, not before. And if you’re debugging why an automated run looks wrong on a real device rather than an emulator, mirroring the screen live with scrcpy while the test runs is a faster way to see what UI Automator is actually clicking than reading a failure log after the fact. For everything else adb-adjacent that comes up while wiring these suites together, the rest is on the blog.

Written by Xavier Fok

disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-09-16.

free download
The adb and scrcpy command cheat sheet

The commands we reach for on a rack of real phones, on one printable page. Leave your email and we will also tell you when we publish a new guide, a few times a month at most.

from the team behind cloudf.one
Passes on the emulator, fails on a real phone?

cloudf.one rents real Android phones in Singapore that you control from the browser, each on a persistent Singapore mobile IP. Real hardware, a real OEM build and a real carrier network, without buying and racking the phones yourself.

test on a real phone →
read on
More from ADB Handbook

adb commands, scrcpy flags, automation frameworks, and why a test passes on one phone and fails on the next.

browse all articles →