View all services
Talk to QA Advisor
Browse the Knowledge Hub211 resources
/Tools Library/Espresso
Tools Library · Review and guide

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

Reviewed by Viral Patel

Co-Founder, QAbleLast reviewed 13 October 2026

At a glance

Espresso at a glance

Official site
Vendor
Google
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

  1. Open the app module’s build.gradle.kts and set testInstrumentationRunner to androidx.test.runner.AndroidJUnitRunner.
  2. Add the Espresso, AndroidX JUnit and runner dependencies to androidTestImplementation.
  3. Turn off window, transition and animator animations on the test device in Developer options.
  4. Create a test class under src/androidTest with an ActivityScenarioRule for the screen you want to start on.
  5. 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 connectedAndroidTest

A 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/androidTests

Isolation, 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 pixel6api34DebugAndroidTest

Pitfalls

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

ToolWho usually writes testsTest languagesProcess modelSynchronisationPlatforms
EspressoAndroid developersKotlin, JavaInside the app processAutomatic main-thread idle checksAndroid only
AppiumQA engineers and SDETsJava, Python, JS, C#, RubyExternal server over WebDriverExplicit waits in test codeAndroid, iOS, desktop
MaestroTesters, developers and agentsYAML, optional JavaScriptBlack box via accessibility layerBuilt-in settle waits and retriesAndroid, iOS simulators, web
DetoxReact Native developersJavaScript, TypeScriptGrey box, instrumented app buildMonitors the app’s async workReact 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

Resources

Where to go next

Keep going

Prepare, or get it done

Ready

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.

Talk to QA Advisor