Intro to Espresso Testing in Android
Espresso is a fast and reliable UI testing framework for Android apps by Google. It is a compact framework and is part of Android Jetpack. Creating tests for your app is an integral part of the development process. By running tests against your app, you can verify the correctness of its functional behavior and usability before you release it publicly. Testing also provides you with the following advantages:
- Early failure detection of critical user flows in your app during development.
- Safer code refactoring with fewer worries about accidentally adding regressions.
- Programmatically run your app across different Android Emulators or device types.
Writing a test case using Espresso requires an understanding of its core API and the ability to set it up within Android Studio. We will cover both of these topics in this guide.
Why use Espresso?
If you are working on an Android app project and have access to the source code, writing Espresso tests can provide fast test suite execution times and have a tendency to be less flaky. This is mainly due to two reasons:
- Synchronization Capabilities: Espresso tests ensure that before the test starts the UI or Activity being testes has already finished starting. It ensures that all Background Processes related to the application have been finished. In fact, it gives you the capability of defining Idling Resource which makes the framework aware of the custom background processes started by any user action. All these capabilities reduce the need for wait times which can introduce a lot of implementation complexity and flakiness to test cases.
- Intent Stubbing: Android Intents is a mechanism via which your application can communicate with the Android System and other 3rd party applications. Espresso allows you to control Android Intents behavior so that they can be stubbed out to provide pre-determined responses. Tests end up being reliable when there is reduced dependence on the changes to the external applications introduced with every release. Stubbed or Mocked Intents will also eliminate instances where your automation test fails due to a bug in an external application.
Setup
To prepare your testing environment, you need an emulator with animations turned off and the application that you want to test opened in Android Studio.
Turning off Animations in Emulator
Espresso tests aim for a static state before tests start. As discussed earlier the Espresso framework accomplishes this using its synchronization capabilities. However, animations introduce another factor that can add flakiness to a test. Hence, it is recommended to turn off these animations for running your Espresso test.
- Go to Settings > Developer Options.
- Turn off Window animation scale, Transition animation scale, and Animator duration scale.
Android Studio Setup
As part of the Android Studio setup, you will need to edit the build.gradle
file for your application. This file can be found at app/build.gradle
. This file will have many sections like plugins, android, and dependencies. You will add the following lines to the dependencies
section.
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
Now that you have added the dependencies for Espresso, you need to select a test runner for your apps. Espresso tests are conventionally written in JUnit 4 and to avoid re-inventing wheels, we will be using the same test runner. You will need to add the following line to the android
section in the defaultConfig
sub-section.
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
In the end, your app/build.gradle
file should look like this. You should see a banner on the top alerting you about changes to the build.gradle
file. You should click Sync Now
to download the newly added dependecies.
Understanding Espresso API
There are four main types of API components.
- Espresso — These function are either commonly used methods like
OnView()
,OnData()
or android system's interactions that don't have anything to with views likepressBack()
. - ViewMatchers — Similar to Appium’s selector strategies, these matchers can be used — either alone or in tandem — to identify and find a view.
- ViewActions — After finding a view you need to perform some actions with it. These functions allow you to change the state like clicking a view, scrolling, or entering text.
- ViewAssertions — After you find a view you need to assert whether its state is as expected. You can do this by utilizing the final set of Espresso’s API components to perform assertions against the state of views during the test. For example, checking if a list has X number of elements.
Finding an element
To find an element in a view hierarchy, we need to identify its unique characteristics. These can either be it's id
or its contentDescription
. These characteristics are selected using functions called matchers. For example, withId(R.id.loginButton)
uses a resource id matcher to select the login button. Using this matcher we can find the login button like this.
OnView(withId(R.id.loginButton));
The Android documentation lists other matchers as well and you can click here to find out about these.
Finding elements within AdapterView
The OnView()
function can, on occasion, fail to find an element within an AdapterView
if the element in question is not visible within the bounding box of the screen. If the element is part of a list and you need to scroll to in order to click it, OnView()
will fail. It is therefore better practice to use OnData()
to find elements within AdapterView
. OnData()
as the name suggests looks for information beyond the bounding box instead of specific characteristics. It also does the scrolling for you if required.
onData(allOf(is(instanceOf(String.class)), is("LOGIN")))
The above sample matches an element with the String “LOGIN” as part of its data.
Performing Actions
After you successfully find your element you need to perform some actions with it. This can be done using ViewActions
. If you want to click an element, you need the following line.
onView(...).perform(click());
.perform()
can take multiple arguments. It will perform these actions one by one. For example, you might need to enter text and then click the button.
onView(...).perform(typeText("Hello"), click());
You can find a reference of other useful actions in this page of the android docs.
Asserting States of the Views
You can assert some basic states or characteristics of the views using ViewAssertions
. If you want to verify that a text field has some pre-determined text, you can use the check()
method and pass it to a matcher.
onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")));
The above few paragraphs detail the functioning of the core Espresso API. Espresso also has other useful APIs for intent stubbing, idling resources and WebViews which we can discuss in future posts.
Writing the test case
Open Android Studio. You will find that your Android project has a similar directory structure
├── app
│ ├── build.gradle
│ ├── libs
│ └── src
│ ├── androidTest
│ ├── main
│ └── test
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── local.properties
└── settings.gradle
Under app/src
directory, there are 3 directories.
main
: This directory contains the source code for the application.test
: This directory contains unit tests for your application.androidTest
: This directory contains Espresso or other Instrumentation tests.
In androidTest
, create a new Java Class called ExampleIntrumentedTest
. Add the following lines to that file.
package com.example.espressosample;import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;import com.example.espressosample.ui.login.LoginActivity;import static org.hamcrest.Matchers.*;import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;import static androidx.test.espresso.Espresso.*;
import static androidx.test.espresso.matcher.ViewMatchers.*;
import static androidx.test.espresso.assertion.ViewAssertions.*;@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Rule
public ActivityScenarioRule<LoginActivity> activityRule =
new ActivityScenarioRule<>(LoginActivity.class); @Test
public void LoginBtnDisabled_IfUserEmpty() {
// Assert that username is empty
onView(withId(R.id.username)).check(matches(withText(isEmptyString())));
// Assert that Login button is disabled
onView(withId(R.id.login)).check(matches(not(isEnabled())));
}
}
Let’s dissect the above information.
RunWith
: This annotation tells the compiler that this is a JUnit 4 test case.ActivityScenarioRule
: Just likebeforeAll
andafterAll
, a JUnit rule simply put abstracts away some test-related setup that is repetitive in nature. An example of this would be launching a given activity before the test starts and close it after the test. This is exactly whatActivityScenarioRule
does. You can scale this and create your own rules to avoid repeating test boilerplate code as well.@Test
: This method contains all the test case logic of finding elements, performing actions, and validating using assertions. In thisLoginBtnDisabled_IfUserEmpty
, we first assert that the username field is empty and then assert that the login button is disabled. Notice the use ofnot()
Hamcrest matcher withisEnabled()
. This is how to use matchers in tandem.
Running the Test Case
You can either run the test case using Android Studio or command line
Android Studio
- Click the double play icon on line
public class ExampleInstrumentedTest {
of your test case. - Click on
Create
. This will open a run configuration menu. - Click
Ok
. - Click
Run
in the toolbar or enter^ R
on Mac.
Command Line
- To run it from the command line, you just need to execute the gradle wrapper from the root of your application directory.
./gradlew connectedAndroidTest
Conclusion
This was a short introduction to writing an Espresso test. Hopefully, now you can add reliable UI tests to your testing arsenal in addition to Unit Tests and Appium End to End tests. In the future, we will also be covering how to utilize IdlingResource
and stub Android Intents while using intended()
function. Thanks for reading!