Espresso vs UIAutomator: Which Android Testing Framework to Choose
Espresso and UIAutomator test Android apps at different levels. Espresso runs inside your app process and has access to app internals — it's the right choice for in-app UI testing. UIAutomator runs outside your app and can interact with system UI, other apps, and device controls — it's the right choice for system-level interactions. Most apps need both.
Key Takeaways
Espresso for your app, UIAutomator for everything else. Espresso can't interact with system dialogs (permissions, notifications) or other apps. UIAutomator can, but can't access your app's internal resource IDs.
Espresso auto-synchronizes; UIAutomator does not. Espresso waits for the UI thread automatically. UIAutomator requires explicit waits (UiObject2.wait(), UiDevice.waitForIdle()).
Both can be used in the same test. A single Espresso test can trigger an action that launches a system dialog, then use UIAutomator to handle that dialog, then return to Espresso for the rest of the test.
UIAutomator is slower. Because it runs in a separate process and uses the Accessibility service (not direct view access), UIAutomator interactions take longer than equivalent Espresso interactions.
UIAutomator2 is the modern version. com.android.uiautomator is deprecated. Use androidx.test.uiautomator (UIAutomator2) in all new tests.
Architecture Comparison
Understanding the architectural difference explains when to use each framework.
Espresso Architecture
Espresso runs in the same process as your app (technically as an instrumentation process that shares memory with the app):
[Test Process + App Process]
├── Your App Code
├── Espresso Test Code
│ ├── Direct access to R.id.* resource IDs
│ ├── Direct access to View objects
│ └── UI thread synchronization
└── [No access to system UI or other apps]Consequence: Fast, reliable, with access to app internals. Can't see outside the app.
UIAutomator Architecture
UIAutomator runs in a separate process and communicates with the Accessibility service:
[Test Process] ← Accessibility Service → [Your App Process]
[Test Process] ← Accessibility Service → [System UI Process]
[Test Process] ← Accessibility Service → [Other App Processes]Consequence: Can control anything on screen, across all apps and system UI. Slower, no access to app-internal resource IDs.
API Comparison
Finding Elements
Espresso:
// By resource ID (fast, reliable)
onView(withId(R.id.submit_button))
// By text
onView(withText("Submit"))
// By content description
onView(withContentDescription("Close"))UIAutomator:
UiDevice device = UiDevice.getInstance(getInstrumentation());
// By content description (most reliable)
UiObject2 button = device.findObject(By.desc("Submit"));
// By text
UiObject2 button = device.findObject(By.text("Submit"));
// By resource ID (full ID including package)
UiObject2 button = device.findObject(By.res("com.example.app", "submit_button"));
// By class name
UiObject2 textView = device.findObject(By.clazz(TextView.class));Note the difference in resource ID: Espresso uses R.id.submit_button (compile-time constant); UIAutomator uses the string "com.example.app:id/submit_button" at runtime.
Interactions
Espresso:
onView(withId(R.id.search_input))
.perform(typeText("query"), pressImeActionButton());
onView(withId(R.id.submit_button))
.perform(scrollTo(), click());UIAutomator:
UiObject2 searchInput = device.findObject(By.res("com.example.app", "search_input"));
searchInput.setText("query");
UiObject2 submitButton = device.findObject(By.res("com.example.app", "submit_button"));
submitButton.click();Waiting for Conditions
Espresso:
// Automatic — Espresso waits for UI thread idle
onView(withId(R.id.result)).check(matches(isDisplayed()));
// If result appears asynchronously, Espresso waits (with IdlingResource)UIAutomator (manual waiting required):
// Wait for element to appear (up to 5 seconds)
UiObject2 result = device.wait(Until.findObject(By.res("com.example.app", "result")), 5000);
assertThat(result).isNotNull();
// Wait for element to be gone
device.wait(Until.gone(By.res("com.example.app", "loading_spinner")), 10000);
// Wait for any UI activity to settle
device.waitForIdle(5000);When to Use Espresso
Use Espresso for testing your app's screens and flows:
- User login and registration
- Form validation
- Navigation between screens
- List interactions (RecyclerView, ListView)
- State changes (button enabled/disabled, text changing)
- In-app animations and transitions
@Test
public void addItemToCartUpdatesCartCount() {
// Navigate to product detail
onView(withId(R.id.product_list))
.perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
// Add to cart
onView(withId(R.id.add_to_cart_button)).perform(click());
// Cart count updates
onView(withId(R.id.cart_count_badge))
.check(matches(withText("1")));
}When to Use UIAutomator
Use UIAutomator for anything involving system UI or cross-app interactions:
Handling Permission Dialogs
@Test
public void requestingLocationPermissionShowsSystemDialog() {
// Grant location permission using UIAutomator
UiDevice device = UiDevice.getInstance(getInstrumentation());
// Trigger location request (via Espresso)
onView(withId(R.id.enable_location_button)).perform(click());
// Permission dialog is system UI — Espresso can't access it
// UIAutomator can
UiObject2 allowButton = device.wait(
Until.findObject(By.text("Allow only while using the app")),
3000
);
assertThat(allowButton).isNotNull();
allowButton.click();
// Back to Espresso for in-app assertions
onView(withId(R.id.location_enabled_indicator))
.check(matches(isDisplayed()));
}Testing Notifications
@Test
public void orderConfirmationNotificationAppears() {
UiDevice device = UiDevice.getInstance(getInstrumentation());
// Trigger order completion (via Espresso)
onView(withId(R.id.confirm_order_button)).perform(click());
// Open notification shade
device.openNotification();
// Find notification using UIAutomator
UiObject2 notification = device.wait(
Until.findObject(By.textContains("Order confirmed")),
10000
);
assertThat(notification).isNotNull();
// Tap notification to open app
notification.click();
// Verify app navigated correctly
onView(withId(R.id.order_confirmation_screen))
.check(matches(isDisplayed()));
}Interacting with Other Apps
@Test
public void sharingContentOpensShareSheet() {
UiDevice device = UiDevice.getInstance(getInstrumentation());
// Trigger share from your app
onView(withId(R.id.share_button)).perform(click());
// Share sheet is a system component
UiObject2 shareSheet = device.wait(
Until.findObject(By.res("android", "title").textContains("Share")),
3000
);
assertThat(shareSheet).isNotNull();
// Select a specific app from share sheet
UiObject2 messagesApp = device.findObject(By.desc("Messages"));
if (messagesApp != null) {
messagesApp.click();
// Handle messages app...
}
}Using Both in a Single Test
The most common pattern combines both frameworks:
@RunWith(AndroidJUnit4.class)
public class PhotoUploadTest {
UiDevice device;
@Before
public void setUp() {
device = UiDevice.getInstance(getInstrumentation());
}
@Test
public void uploadPhotoAfterGrantingPermission() {
// 1. Navigate to upload screen (Espresso)
onView(withId(R.id.profile_tab)).perform(click());
onView(withId(R.id.edit_photo_button)).perform(click());
// 2. Handle permission dialog (UIAutomator)
UiObject2 allowButton = device.wait(
Until.findObject(By.text("Allow")),
3000
);
if (allowButton != null) {
allowButton.click();
}
// 3. Photo picker opens (UIAutomator — system UI)
UiObject2 firstPhoto = device.wait(
Until.findObject(By.res("com.android.providers.media.photopicker", "icon_thumbnail")),
3000
);
assertThat(firstPhoto).isNotNull();
firstPhoto.click();
// 4. Verify photo appears in app (Espresso)
onView(withId(R.id.profile_photo))
.check(matches(isDisplayed()));
}
}Performance Comparison
| Aspect | Espresso | UIAutomator |
|---|---|---|
| Speed per interaction | Fast (~50ms) | Slower (~200ms) |
| Element resolution | Resource IDs (compile-time) | Accessibility IDs (runtime) |
| UI thread synchronization | Automatic | Manual (waitForIdle) |
| System UI access | No | Yes |
| Cross-app testing | No | Yes |
| Access to app internals | Yes | No |
| Flakiness | Low | Higher (timing dependent) |
Gradle Setup for UIAutomator
dependencies {
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
}For tests that use both:
dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
}Decision Guide
Use Espresso when:
- Testing screens and flows within your app
- Testing view state (visibility, enabled state, text content)
- Testing RecyclerView interactions
- Testing navigation
- Testing form validation
Use UIAutomator when:
- Handling system permission dialogs
- Testing notification behavior
- Testing interactions with system settings
- Cross-app flows (your app → maps app → back)
- Testing app behavior from the home screen (fresh launch)
- Deep linking tests
Use both when:
- Your flow involves both in-app screens and system UI (common for camera, location, contacts)
- You need to test the full lifecycle of a cross-app interaction
Most production Android test suites use Espresso as the primary framework and UIAutomator only where Espresso falls short: system dialogs and notifications. This gives you the reliability and speed of Espresso for the majority of tests, with UIAutomator's reach for the cases that require it.