Espresso Android Testing: Getting Started with UI Tests in Android Studio

Espresso Android Testing: Getting Started with UI Tests in Android Studio

Espresso is Google's official Android UI testing framework. It's built into the Android Testing Support Library, runs directly on the device or emulator, and provides a concise API for interacting with and asserting on your app's UI.

Unlike Appium or UiAutomator that use external drivers, Espresso runs in the same process as your app. This gives it two major advantages: it knows when the UI is idle (no flaky waits), and it can access app internals like view hierarchies and context directly.

Project Setup

Add Espresso to your build.gradle:

// app/build.gradle
android {
    defaultConfig {
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
}

dependencies {
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test:runner:1.5.2'
    androidTestImplementation 'androidx.test:rules:1.5.0'
}

Espresso tests live in src/androidTest/java/ — the instrumented test directory that runs on a device/emulator, not on your local JVM.

Your First Espresso Test

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
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class LoginActivityTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Test
    fun login_withValidCredentials_navigatesToDashboard() {
        // Type username
        onView(withId(R.id.editTextUsername))
            .perform(typeText("testuser"), closeSoftKeyboard())

        // Type password
        onView(withId(R.id.editTextPassword))
            .perform(typeText("password123"), closeSoftKeyboard())

        // Click login button
        onView(withId(R.id.buttonLogin))
            .perform(click())

        // Verify navigation to dashboard
        onView(withId(R.id.dashboardTitle))
            .check(matches(isDisplayed()))
    }
}

The three core building blocks:

  • onView(matcher) — find a view using a matcher
  • .perform(action) — perform an action on the matched view
  • .check(assertion) — assert something about the matched view

ViewMatchers: Finding Views

ViewMatchers locate views in the hierarchy:

// Find by resource ID (most common)
onView(withId(R.id.submitButton))

// Find by text
onView(withText("Submit"))
onView(withText(R.string.submit_label))  // from resources

// Partial text match
onView(withText(containsString("Submit")))

// Find by content description (accessibility label)
onView(withContentDescription("close button"))
onView(withContentDescription(R.string.close_content_description))

// Find by class
onView(isAssignableFrom(Button::class.java))

// Find by hint text (for empty EditText)
onView(withHint("Enter email address"))

// Combined matchers using allOf
onView(allOf(withId(R.id.textView), withText("Error: invalid email")))

// anyOf — matches if any condition is true
onView(anyOf(withText("OK"), withText("Accept")))

// Parent/child relationships
onView(allOf(withId(R.id.label), isDescendantOfA(withId(R.id.userCard))))

ViewActions: Interacting with Views

ViewActions define what to do with the matched view:

// Clicks
onView(withId(R.id.button)).perform(click())
onView(withId(R.id.button)).perform(longClick())
onView(withId(R.id.button)).perform(doubleClick())

// Text input
onView(withId(R.id.editText)).perform(typeText("Hello"))
onView(withId(R.id.editText)).perform(typeTextIntoFocusedView("Hello"))
onView(withId(R.id.editText)).perform(clearText())
onView(withId(R.id.editText)).perform(replaceText("New text"))

// Keyboard
onView(withId(R.id.editText)).perform(closeSoftKeyboard())
onView(withId(R.id.editText)).perform(pressImeActionButton())

// Scrolling
onView(withId(R.id.scrollView)).perform(scrollTo())
onView(withText("Item 50")).perform(scrollTo())

// Swiping (in ViewPager, etc.)
onView(withId(R.id.viewPager)).perform(swipeLeft())
onView(withId(R.id.viewPager)).perform(swipeRight())
onView(withId(R.id.refreshLayout)).perform(swipeDown())

// Chaining actions
onView(withId(R.id.searchField))
    .perform(click(), typeText("espresso"), pressImeActionButton())

ViewAssertions: Verifying State

ViewAssertions check the state of matched views:

// Visibility
onView(withId(R.id.errorMessage)).check(matches(isDisplayed()))
onView(withId(R.id.loadingSpinner)).check(matches(not(isDisplayed())))
onView(withId(R.id.hiddenView)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))

// Text content
onView(withId(R.id.titleText)).check(matches(withText("Welcome")))
onView(withId(R.id.countLabel)).check(matches(withText(containsString("3 items"))))

// Enabled/disabled state
onView(withId(R.id.submitButton)).check(matches(isEnabled()))
onView(withId(R.id.submitButton)).check(matches(not(isEnabled())))

// Checked state (for CheckBox, RadioButton)
onView(withId(R.id.agreeCheckbox)).check(matches(isChecked()))
onView(withId(R.id.optionB)).check(matches(isNotChecked()))

// Selected (Spinner, ListView)
onView(withId(R.id.spinner)).check(matches(withSpinnerText("Option A")))

// View doesn't exist
onView(withId(R.id.errorDialog)).check(doesNotExist())

// Count of matching views
onView(withId(R.id.listItem)).check(matches(isDisplayed()))
// For counting: use ViewInteraction.check with a custom assertion

ActivityScenarioRule vs ActivityTestRule

ActivityTestRule is the older API (deprecated). Use ActivityScenarioRule:

// Modern approach
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)

// Access the activity in a test
@Test
fun test_withActivityAccess() {
    activityRule.scenario.onActivity { activity ->
        // Interact with activity directly
        activity.someMethod()
    }
}

// Launch with a specific Intent
@get:Rule
val activityRule = ActivityScenarioRule<MainActivity>(
    Intent(ApplicationProvider.getApplicationContext(), MainActivity::class.java).apply {
        putExtra("mode", "test")
    }
)

Testing RecyclerView

RecyclerView requires the espresso-contrib library:

androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
import androidx.test.espresso.contrib.RecyclerViewActions

// Scroll to an item
onView(withId(R.id.recyclerView))
    .perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(10))

// Scroll to item matching a condition
onView(withId(R.id.recyclerView))
    .perform(RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
        hasDescendant(withText("Target Item"))
    ))

// Click on item at position
onView(withId(R.id.recyclerView))
    .perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(3, click()))

// Click on an item that matches a condition
onView(withId(R.id.recyclerView))
    .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
        hasDescendant(withText("Target Item")), click()
    ))

// Click on a child view within an item
onView(withId(R.id.recyclerView))
    .perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
        2,
        ViewActions.actionWithAssertions(
            clickChildViewWithId(R.id.deleteButton)
        )
    ))

Testing Navigation (Jetpack Navigation)

With the Navigation component:

androidTestImplementation 'androidx.navigation:navigation-testing:2.7.6'
@Test
fun clicking_navigate_button_shows_second_fragment() {
    val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
    
    launchFragmentInContainer<FirstFragment>().onFragment { fragment ->
        Navigation.setViewNavController(fragment.requireView(), navController)
    }
    
    onView(withId(R.id.navigateButton)).perform(click())
    
    assertThat(navController.currentDestination?.id).isEqualTo(R.id.secondFragment)
}

Running Espresso Tests

From Android Studio

Right-click on the test class or test method and select "Run 'LoginActivityTest'". Tests run on the connected device or selected emulator.

From Command Line

# Run all instrumented tests
./gradlew connectedAndroidTest

# Run specific class
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.LoginActivityTest

# Run specific method
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.LoginActivityTest#login_withValidCredentials_navigatesToDashboard

Test Reports

After running, find the HTML report at:

app/build/reports/androidTests/connected/index.html

Common Patterns

Setup and Teardown

@RunWith(AndroidJUnit4::class)
class CheckoutTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(CheckoutActivity::class.java)

    @Before
    fun setup() {
        // Set up test data, mock dependencies, etc.
        Intents.init()  // If testing intents
    }

    @After
    fun teardown() {
        Intents.release()
    }
}

Testing Dialogs

@Test
fun confirmDelete_showsDialog_and_confirms() {
    onView(withId(R.id.deleteButton)).perform(click())
    
    // Dialog appeared
    onView(withText("Confirm Delete?")).check(matches(isDisplayed()))
    
    // Click the confirm button in the dialog
    onView(withId(android.R.id.button1)).perform(click())  // Positive button
    
    // Verify deletion happened
    onView(withId(R.id.itemList)).check(matches(hasChildCount(0)))
}

Testing Toasts

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.RootMatchers.isToast

@Test
fun saving_showsSuccessToast() {
    onView(withId(R.id.saveButton)).perform(click())
    
    onView(withText("Saved successfully"))
        .inRoot(isToast())
        .check(matches(isDisplayed()))
}

Summary

Espresso is the right tool for Android UI testing when you need to test your own app's UI in isolation. The synchronization model (automatic waiting for the UI to be idle) eliminates the main source of flakiness in UI tests. The matcher API is expressive and the view hierarchy access is direct.

The key limitations: it can only test your own app (no cross-app interaction — that requires UiAutomator), and it requires a real device or emulator to run. For unit testing business logic, use JUnit 4 or JUnit 5 in the test/ directory (local JVM tests) instead.

Read more

Start now free