Espresso is a popular open-source UI testing framework, developed and maintained by Google, designed specifically for Android developers. It provides a simple core API that makes it easy to write concise and reliable UI tests for Android apps, even for developers with no previous testing experience. Its core API remains open for customization, making Espresso a powerful testing tool you can depend on for the quality and reliability of your Android apps.
In this topic, we will cover the basics of Espresso and get you started with writing Espresso tests for your Android apps.
Why Espresso?
As mentioned earlier, Espresso provides a simple-to-learn core API which is also open for customizing. Espresso is integrated into Android Studio, which makes it even easier to create, run and debug UI tests from within the development environment. Additionally, there are features such as Record Espresso Test that can be used to generate a test based on user interaction with the app under testing (we'll cover it in later topics). Moreover, a new Android Studio project comes with added dependencies for Espresso, which is a step towards making writing Espresso tests easier.
One of the key benefits of using Espresso is its synchronization capabilities as it is a known challenge in UI testing when dealing with the asynchronous nature of user interactions and app behaviors. Espresso provides a range of synchronization mechanisms that enable tests to wait for some conditions to be met before proceeding to the next test step. This will be covered in detail in other Espresso topics.
All these advantages combine to make Espresso one of the most popular testing frameworks in Android development for writing concise and reliable UI tests.
Setting up Espresso
Espresso should already be configured by default when you create an Android Studio project. To confirm, check for the presence of these dependencies in your app's build.gradle file:
dependencies {
...
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}You can check the latest version of the espresso-core library and the latest version Android extension library for JUnit. Here is why we need those dependencies:
androidx.test.espresso:espresso-coredependency provides the core API for writing UI tests using Espresso in Android applications.androidx.test.ext:junitdependency provides extensions for using JUnit in Android app testing. It is required because Espresso relies on the JUnit testing framework for the execution of its test cases.
Next, verify if the instrumentation runner is set in the same build.gradle file:
android {
...
defaultConfig {
...
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}The instrumentation runner is a test runner that executes instrumentation tests on an Android device or emulator. It is responsible for running the test cases and collecting the test results.
Finally, you need to set up your test device(s) for UI testing. The first step is to enable developer options on the device. If you are unsure of how to do it, you can search for device-specific instructions on the internet. Once the developer options are enabled, disable these three settings: window animation scale, transition animation scale, and animator duration scale. This step is necessary to reduce test flakiness.
Test flakiness refers to the unpredictable and inconsistent behavior of test results due to some external factors.
Basic test structure
In this topic, we'll create Espresso tests for the Espresso Basics app. It's a simple application that showcases formatted text entered by the user. The app shows the well-known "Hello World!" text upon launching. Users can then enter a text, select a formatting option, and click the "Display" button to display the formatted text. The app trims and formats the input text before displaying it on the screen. If the input text is blank, the app displays the message "Empty text, please enter some text and try again!" without formatting it. You can proceed to build and run the app to become more familiar with its functionalities and interface. Here is a preview of the app:
Espresso has a structure similar to JUnit tests because it is based on JUnit, but it includes additional methods that allow us to interact with the user interface. Here is an example Espresso test for our app:
Before we start testing with Espresso, we must understand what we should be testing. In particular, we must distinguish between UI and unit tests, as they serve different purposes. While unit tests are designed to test the business logic of our app, UI tests are intended to ensure that the app's user interface works as expected. For instance, we might use Espresso to test whether our app displays the "Hello World!" greeting when it launches and if we can type a message, select a formatting option, and have it properly displayed. However, we wouldn't use Espresso to test the logic that trims and format the message before it's displayed — this should be tested in unit tests. In a well-structured app, separating the two types of tests should be easy.
Espresso has a structure similar to JUnit tests because it is based on JUnit, but it includes additional methods that allow us to interact with the user interface. Here is an example Espresso test for our app:
@RunWith(AndroidJUnit4::class)
class EspressoBasicsTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testGreetingIsDisplayedOnAppLaunch() {
onView(withText(R.string.greeting))
.check(matches(isDisplayed()))
}
@Test
fun testInputIsCorrectlyDisplayed() {
val inputText = "Jetbrains Academy"
val expectedText = "JETBRAINS ACADEMY"
onView(withId(R.id.et_text))
.perform(typeText(inputText), closeSoftKeyboard())
onView(withId(R.id.rb_uppercase))
.perform(click())
onView(withId(R.id.bt_display))
.perform(click())
onView(withId(R.id.tv_display))
.check(matches(withText(expectedText)))
}
}Note that we are using camelCase (snake_case is also allowed) to name our test methods instead of Kotlin's convention of using backticks, which allows for spaces in function names. This is because Android's runtime did not support spaces in function names until API 30. While you can still use backticks, it is outside the scope of this topic.
Let's break down the code. As you may recall, UI tests are instrumented tests. Therefore, we have to specify the instrumentation test runner. The @RunWith annotation from org.junit.runner.RunWith is used to specify the test runner. The test runner is in charge of executing the test cases on a physical device or emulator. In this case, we are using the AndroidJUnit4 class from androidx.test.ext.junit.runners.AndroidJUnit4 as the test runner which is the standard runner for Android. This runner is created explicitly for testing Android apps and includes many features for running instrumentation tests, such as the ability to manage the lifecycle of the Android application being tested, inject dependencies, and capture screenshots and logs.
The EspressoBasicsTest class holds our test cases. Before we execute the test cases, we first need to launch our app. You can do it by specifying a JUnit rule. The @get:Rule annotation is used to declare a JUnit rule. In this case, we are targeting a getter method with the annotation rather than a field. The JUnit's rule annotation is imported from org.junit.Rule. We are using the ActivityScenarioRule, a JUnit rule that launches an activity before a test starts and finishes it after the test ends.
Next, we specified two test methods using the @Test annotation from org.junit.Test which you should be familiar with. Here is where Espresso comes in. Espresso provides the onView() method from androidx.test.espresso.Espresso.onView, which is our entry point to interact with views. onView() takes in a ViewMatcher, a hamcrest matcher used to locate views in the current view hierarchy. It then returns a ViewInteraction object with which we can use the method perform() to perform actions on the matched view or check() to make assertions. The perform() method takes in ViewActions, whereas the check() method takes in a ViewAssertion. The following sections will detail the three components: ViewMatchers, ViewActions, and ViewAssertions.
One challenge with writing Espresso tests in Android Studio is that methods only autocomplete if they are imported.
ViewMatchers
ViewMatchers, as the name suggests, are basically matcher objects for matching views that implement the Matcher<View> interface from the Hamcrest library. They are used to specify criteria or conditions for finding views within the current view hierarchy. A view must meet the specified conditions in order to be matched. You can pass one or more ViewMatchers to the onView() method which returns a ViewInteraction object that you can use to perform actions or check assertions on the matched view.
There are many built-in methods that return ViewMatchers in Espresso (Note that the methods should be imported from androidx.test.espresso.matcher.ViewMatchers.*), some of which include:
| Matches a view with the provided ID |
| Matches a view with the provided text |
| Matches a view with the provided content description |
| Matches a view that is enabled |
| Matches a view that is checked for example when using a checkbox |
| Matches a view that is partially or completely displayed in the current view hierarchy |
| Matches a view that is completely displayed in the current view hierarchy |
You can also combine multiple matchers using combination matchers such as allOf() or anyOf() and inverse a matcher using not(). For example, you can write:
onView(allOf(withId(R.id.button), not(withText("OK")), isDisplayed()))The code snippet above will find a view that has an ID of R.id.button, not with the text "OK", and is currently visible on the screen. The isDisplayed() method will match even if a view is partially visible on screen. To ensure that a view is completely visible, use the isCompletelyDisplayed() method instead. Note that when you use matchers, you should only match one view in the current view hierarchy. Otherwise, you will end up with an AmbiguousViewMatcherException. The exception message provides a text representation of the current view hierarchy, which you can use to narrow your search to one view. In cases where no view is matched, a NoMatchingViewException will be thrown. In the vast majority of cases, the methods withId() and withText() will suffice, which is the case with our app's test code:
// In the function testGreetingIsDisplayedOnAppLaunch()
onView(withText(R.string.greeting))
// In the function testInputIsCorrectlyDisplayed()
onView(withId(R.id.et_text))
onView(withId(R.id.rb_uppercase))
onView(withId(R.id.bt_display))
onView(withId(R.id.tv_display))You can also create your own custom ViewMatchers by implementing the Matcher<View> interface or extending an existing ViewMatcher class. More on this will be covered in other Espresso topics.
ViewActions
ViewActions are objects that can be passed to the ViewInteraction.perform() method to perform an action on a view, such as clicking, typing, scrolling, and so on. Espresso provides many built-in methods that return ViewActions that cover common UI interactions. You can find them in the androidx.test.espresso.action.ViewActions class. Some examples are:
| Performs a single click on the view |
| Performs a double click on the view |
| Performs a long click on the view |
| Types the provided text into an |
| Clears any text from an |
| Performs a swipe left gesture on the view |
| Performs a swipe right gesture on the view |
You can also chain multiple ViewActions together by using commas. For example:
onView(withId(R.id.edit_text))
.perform(clearText(), typeText("Hello World!"))The code snippet above clears any text from the EditText with the ID R.id.edit_text before typing "Hello World!". Back to our app's test example:
// In function testInputIsCorrectlyDisplayed()
// We type the String stored in the inputText variable into the EditText
// with the ID 'R.id.et_text'
// We will touch on closeSoftKeyboard() later in the topic
onView(withId(R.id.et_text))
.perform(typeText(inputText), closeSoftKeyboard())
// We click the RadioButton with the ID 'R.id.rb_uppercase'
onView(withId(R.id.rb_uppercase))
.perform(click())
// We click the Button with the ID 'R.id.bt_display'
onView(withId(R.id.bt_display))
.perform(click())Sometimes, you may need to perform an action that is not provided by Espresso or customize an existing one. In that case, you can create your own custom ViewAction by implementing the ViewAction interface or extending one of its subclasses (such as GeneralClickAction). This will be covered in other Espresso topics.
ViewAssertions
ViewAssertions are objects that implement the ViewAssertion interface. We can use ViewAssertions on the ViewInteraction.check() method to perform assertions on views, such as checking a view's visibility, text, color, and so on. The matches() method from androidx.test.espresso.assertion.ViewAssertions is the most basic method that returns a ViewAssertion object that can be used to make assertions. The matches() method is a helper method that wraps a ViewMatcher object into a ViewAssertion object. This means that you can pass ViewMatchers into the matches() object as is the case with our app's test code:
// In the function testGreetingIsDisplayedOnAppLaunch()
// Checks if the matched view is displayed
onView(withText(R.string.greeting))
.check(matches(isDisplayed()))
// In the function testInputIsCorrectlyDisplayed()
// Checks if the matched view's text matches the String in the expectedText variable
onView(withId(R.id.tv_display))
.check(matches(withText(expectedText)))matches() assertion is used to assert the state of the currently selected view. If the matcher provided inside the matches() method matches the conditions of the view returned when the onView() method was called, then the test passes. Otherwise, it fails.
Running Espresso tests
Running Espresso tests in Android Studio is simple. Just click the green icon beside the test class then Run 'NameOfTestClass' to run the entire test methods in your class or click on the green icon beside a specific test method and then Run 'NameOfTestMethod' to run it:
As you can see, all our tests passed. As the test runs, you can see it executing on the Android device as if an actual user was interacting with the app.
Tips and extras
Espresso provides the closeSoftkeyboard() method, a top-level action that can be used to close the soft keyboard. This is particularly useful when the keyboard covers the view you want to operate on. Note that, while the onView() might be able to locate views loaded in the view hierarchy, you may not be able to perform actions on them using the perform() method if the view is not visible on screen. It's recommended that you close the keyboard when it is not needed. You can also use the scrollTo() method to scroll to a view that is loaded but off the screen:
onView(withId(R.id.view_id))
.perform(scrollTo())
// scrollTo() is not a top level actionWhen using closeSoftKeyboard() or scrollTo(), if the keyboard is already closed or the view is already visible on screen, the methods have no effect.
Other top level actions include pressBack() and openActionBarOverflowOrOptionsMenu(context: Context) (covered in other Espresso topics). Note that top-level actions are not tied to any particular view, for example, you don't need an EditText to press the back button but you need it to perform a typing action. With that being said, you can use these methods wherever you need them in your test method or inside perform():
fun test() {
// You can use it inside perform maybe after clicking a button
onView(...)
.perform(click(), pressBack())
// Or as a standalone
pressBack()
}For adapter views, such as ListView or GridView, the onView() method may not be sufficient, and the onData() method should be used instead. This does not apply to RecyclerView, a more complex adapter view requiring special handling. The onData() method in Espresso takes an ObjectMatcher as an argument and returns a DataInteraction object. The DataInteraction object allows you to perform actions using the perform() method and assertions using the check() method similar to how you would do it when using onView() with some added functionalities. Stay tuned for future topics more throughly explaning working with adapter views.
Conclusion
In this topic, you have learned the basics of Espresso framework, a popular tool for testing Android applications. You have seen how to set up Espresso in our project, write simple test cases using Espresso ViewMatchers, ViewActions, and ViewAssertions, and run them on emulators or real devices. We have also learned some best practices for writing maintainable and reliable Espresso tests. Espresso framework is a powerful and easy-to-use tool that can help us improve the quality and performance of our Android applications by automating the UI testing process.