Espresso vs UIAutomator: Which Android Testing Framework to Choose

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.

Read more