Table of Contents
Introduction
User interface testing with Espresso gives you a way to automatically interact with your app just like a real user would. Instead of manually tapping buttons and typing text for every test, Espresso runs these actions in code, checks what appears on the screen, and verifies that the UI behaves as expected. In this chapter you will learn how Espresso fits into Android testing, how to write basic UI tests, and how to work with common view actions and matchers.
Espresso and Instrumented Tests
Espresso is part of the AndroidX Test library. Espresso tests run on a real device or an emulator, so they are instrumented tests, not plain unit tests. They have access to the full Android framework, activities, views, and resources.
An Espresso test typically launches an activity, finds a view on the screen, performs an action on it, and then verifies some result like a text change or a new view appearing. Espresso automatically handles synchronization with the main thread for you, which means it usually waits for UI events and most background tasks to finish before performing the next step.
Setting Up Espresso Dependencies
To use Espresso, you need to add specific test dependencies to your module level build.gradle (usually app/build.gradle). You only configure these in the androidTestImplementation configuration, because Espresso tests live in the androidTest source set.
A typical setup looks like this:
android {
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
androidTestImplementation "androidx.test.ext:junit:1.1.5"
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
}
The testInstrumentationRunner entry tells Gradle which runner to use when it launches your instrumented tests. espresso-core contains the main Espresso APIs such as onView, withId, perform, and check.
If you use other components, such as RecyclerView, you may add extra Espresso artifacts like espresso-contrib, but espresso-core is the foundation.
Basic Espresso Test Structure
Espresso tests are written as Kotlin or Java classes under the androidTest directory, not inside test. A minimal test file for a login screen might look like this:
package com.example.myapp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.assertion.ViewAssertions.matches
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LoginUiTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun successfulLogin_showsWelcomeMessage() {
onView(withId(R.id.usernameEditText))
.perform(typeText("alice"))
onView(withId(R.id.passwordEditText))
.perform(typeText("password123"))
onView(withId(R.id.loginButton))
.perform(click())
onView(withId(R.id.welcomeTextView))
.check(matches(withText("Welcome, alice!")))
}
}
This example illustrates the typical Espresso pattern. An activity launch rule prepares the screen. Test methods are annotated with @Test. Each interaction is expressed by looking up a view, performing an action, and checking a result.
In Espresso UI tests, the common pattern is:
- Find a view:
onView(matcher) - Perform an action:
.perform(action) - Verify the result:
.check(assertion)
Launching Activities in Tests
Most Espresso tests run against a specific activity. For that you usually use an ActivityScenario or an ActivityScenarioRule. The rule starts the activity before each test and closes it afterward.
The rule looks like this:
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)You pass the activity class that you want to test. When the test begins, the activity starts in a resumed state, ready for interactions. This approach isolates each test case and avoids reusing stale activity instances.
If you want more control, you can use ActivityScenario.launch() directly inside the test body. That can be useful when you need to pass extras in an intent or control the activity lifecycle inside the test.
Finding Views with Matchers
To interact with a view, Espresso needs to find it on the screen. You do this through matchers, which describe properties of the view you are looking for. The simplest matcher uses the view id, defined in your layout files:
onView(withId(R.id.loginButton))You can also match based on visible text:
onView(withText("Login"))
These matchers come from ViewMatchers. Espresso chooses a unique view that satisfies the matcher. If more than one view matches, the test will fail because Espresso cannot decide which one to use.
Sometimes you need more specific criteria. Espresso supports combining matchers through Hamcrest functions like allOf and containsString. An example of combining id and text:
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.containsString
onView(
allOf(
withId(R.id.titleTextView),
withText(containsString("Welcome"))
)
)This selects a view that has a specific id and whose text contains the word "Welcome".
Performing Actions on Views
After you find the view, you perform an action to simulate a user interaction. Espresso defines common actions in ViewActions. These include clicks, typing, scrolling, and closing the soft keyboard.
To simulate a click:
onView(withId(R.id.loginButton))
.perform(click())
To type text into an EditText:
onView(withId(R.id.usernameEditText))
.perform(typeText("alice"))Typing text may leave the soft keyboard open, which sometimes covers other views. A common pattern is to close the keyboard when you finish typing:
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
onView(withId(R.id.passwordEditText))
.perform(typeText("password123"), closeSoftKeyboard())
You can chain multiple actions in one perform call. Espresso executes them in order on the same view.
For long or scrollable content, you may need to scroll to a view before clicking. scrollTo() can be used directly in the action chain when the parent is a scrollable container that supports it.
Verifying Results with Assertions
Assertions in Espresso check that the UI is in the expected state after interactions. You usually call check with matches and a matcher that describes the desired condition.
To verify that a TextView shows a specific text:
onView(withId(R.id.welcomeTextView))
.check(matches(withText("Welcome, alice!")))To verify that a view is displayed:
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
onView(withId(R.id.errorTextView))
.check(matches(isDisplayed()))You can also check that a view does not exist or is not displayed. For example, to verify that an error message does not appear:
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
onView(withText("Error"))
.check(doesNotExist())Or to check that a view is not visible:
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.Visibility
onView(withId(R.id.errorTextView))
.check(matches(withEffectiveVisibility(Visibility.GONE)))Assertions help you express the behavior you expect. Usually each test contains at least one assertion at the end, although multiple assertions in a single test are common when you want to check several related conditions.
Working with EditText and Keyboard
When your UI includes text fields, you often deal with focus and the soft keyboard. Espresso handles most of this automatically, but some patterns repeat often when you test forms.
Typing into an EditText and then pressing the IME action button:
import androidx.test.espresso.action.ViewActions.pressImeActionButton
onView(withId(R.id.searchEditText))
.perform(typeText("kotlin"), pressImeActionButton())This simulates the user pressing "Done" or "Search" on the soft keyboard, depending on the IME options you set in the layout.
If the soft keyboard stays open and covers another view you want to tap, combine text input with closeSoftKeyboard() as earlier, or call Espresso.closeSoftKeyboard() directly before the next action.
Toasts and Snackbars in Espresso
Toasts and Snackbars show brief messages to the user without adding permanent views to the normal hierarchy in the same way as standard views. That makes them slightly trickier to test.
Snackbars usually appear in the same window as your activity under a specific parent, so you can use a text matcher and sometimes combine it with a root matcher. For example, if your UI shows a Snackbar with the text "Item saved", you can try:
onView(withText("Item saved"))
.check(matches(isDisplayed()))This works when the Snackbar is in the same window and not in a dialog or another root.
Toasts often use a different window. You can write a matcher that looks into the Toast root. A common approach is to use inRoot with a special matcher for Toasts, or rely on test-only helpers. Because this requires custom root matchers, many beginners start by testing only permanent widgets like TextView and leave Toast verification for later when they are comfortable with Espresso matchers.
Handling Asynchronous Operations
Espresso automatically waits for the main thread to be idle. That covers many operations, such as animations on the main thread or direct UI updates. However, when your app uses custom background threads or asynchronous work that Espresso does not know about, you may need to tell Espresso when those tasks are busy.
You can do this through IdlingResources. An IdlingResource tells Espresso that the app is busy, then tells it when the app is idle again. When there is at least one busy IdlingResource, Espresso waits before running the next action or assertion.
A very simple implementation outline looks like this:
import androidx.test.espresso.IdlingResource
class SimpleIdlingResource : IdlingResource {
@Volatile
private var callback: IdlingResource.ResourceCallback? = null
@Volatile
private var isIdleNow = true
override fun getName(): String = "SimpleIdlingResource"
override fun isIdleNow(): Boolean = isIdleNow
override fun registerIdleTransitionCallback(
resourceCallback: IdlingResource.ResourceCallback
) {
callback = resourceCallback
}
fun setIdleState(isIdleNow: Boolean) {
this.isIdleNow = isIdleNow
if (isIdleNow) {
callback?.onTransitionToIdle()
}
}
}You register this IdlingResource with Espresso in your test setup and update its state from your app code or test code when async work starts and ends. For beginners, you can often design tests around operations that complete quickly on the main thread or use testing utilities provided by libraries such as Retrofit or Room, which often ship with their own test helpers.
Testing Intents and Navigation
When your UI triggers navigation from one activity to another or sends an implicit intent, Espresso provides integration with the Intents API from AndroidX Test. That allows you to verify that an intent with expected extras or actions has been fired, without necessarily starting the actual destination activity.
The basic pattern is to initialize Intents in a @Before method, perform the action that should send an intent, and then verify the intent properties. While a full configuration involves additional setup beyond Espresso core, it is useful to know that UI testing can also cover intent based navigation.
If you do not use the Intents API, you can still verify navigation indirectly. For example, after a click you check that a new view belonging to the destination screen is displayed. As long as each screen has unique ids or text values, your test can confirm that navigation occurred.
Test Naming and Structure
Readable test names and structures make UI tests easier to maintain and understand. A common naming style for Espresso tests uses a combination of context, action, and result, for example:
loginWithValidCredentials_opensHomeScreen or emptyUsername_showsErrorMessage.
Within each test method, keep the sequence clear. Many developers follow a "given, when, then" style. You prepare initial state, perform an action, then assert the result. For example:
- Given the login screen is visible and fields are empty.
- When the user taps the login button.
- Then an error message appears.
Keeping tests focused on a single scenario and behavior helps reduce flakiness and makes failures easier to interpret.
Running Espresso Tests
Espresso tests run on a device or emulator. In Android Studio you usually run them from the Project tool window or from the test class. You can right click a test method, a test class, or the androidTest package, then choose to run instrumented tests.
When you run a test, Android Studio installs your app and a test apk on the selected device, starts the test instrumentation runner, launches the activity, and executes your Espresso code. The Run window shows passed and failed tests. For failures, you see stack traces and sometimes screenshots if additional tooling is enabled.
You can also run instrumented tests from the command line with Gradle. The typical task is:
./gradlew connectedAndroidTestThis runs all instrumented tests on all connected devices. You may also run tests for a single module or variant through more specific Gradle tasks.
Common Pitfalls and Flakiness
Even simple UI tests can sometimes become flaky, which means they pass occasionally and fail on other runs without code changes. Espresso reduces this problem by waiting for the main thread to be idle, but a few pitfalls remain.
Relying on arbitrary Thread.sleep calls is one of the main sources of flakiness. These pauses are fixed and cannot adapt to slow or fast devices. Instead, prefer Espresso helpers, view assertions that implicitly wait for views to appear, or IdlingResources for long background tasks.
Another pitfall is trying to interact with views that are not yet in the layout or not displayed. If possible, make sure your test navigates correctly to the desired screen and that you use ids that exist in that layout. Assertions like isDisplayed can help confirm that you are on the correct screen before performing additional actions.
Finally, avoid making your tests depend on external services or unstable network conditions. For UI tests that include networking, use fake responses or local test servers. That keeps your UI tests fast and reliable.
Summary
Espresso provides a concise way to describe how a user interacts with your app and what the UI should display in response. With a combination of view matchers, actions, and assertions, you can test login forms, navigation flows, error messages, and many other behaviors. By organizing your tests clearly and paying attention to synchronization with asynchronous work, you can build a suite of UI tests that run automatically and give you confidence that your app behaves correctly when users tap, type, and navigate through your screens.