In this post, you'll learn about how to write UI tests with the Espresso testing framework and automate your test workflow, instead of using the tedious and highly error-prone manual process.
Espresso is a testing framework for writing UI tests in Android. According to the official docs, you can:
Use Espresso to write concise, beautiful, and reliable Android UI tests.
1. Why Use Espresso?
One of the problems with manual testing is that it can be time-consuming and tedious to perform. For example, to test a login screen (manually) in an Android app, you will have to do the following:
- Launch the app.
- Navigate to the login screen.
- Confirm if the
usernameEditText
andpasswordEditText
are visible. - Type the username and password into their respective fields.
- Confirm if the login button is also visible, and then click on that login button.
- Check if the correct views are displayed when that login was successful or was a failure.
Instead of spending all this time manually testing our app, it would be better to spend more time writing code that makes our app stand out from the rest! And, even though manual testing is tedious and quite slow, it is still error-prone and you might miss some corner cases.
Some of the advantages of automated testing include the following:
- Automated tests execute exactly the same test cases every time they are executed.
- Developers can quickly spot a problem quickly before it is sent to the QA team.
- It can save a lot of time, unlike doing manual testing. By saving time, software engineers and the QA team can instead spend more time on challenging and rewarding tasks.
- Higher test coverage is achieved, which leads to a better quality application.
In this tutorial, we'll learn about Espresso by integrating it into an Android Studio project. We'll write UI tests for a login screen and a RecyclerView
, and we'll learn about testing intents.
Quality is not an act, it is a habit. — Pablo Picasso
2. Prerequisites
To be able to follow this tutorial, you'll need:
- a basic understanding of core Android APIs and Kotlin
- Android Studio 3.1.3 or higher
- Kotlin plugin 1.2.51 or higher
A sample project (in Kotlin) for this tutorial can be found on our GitHub repo so you can easily follow along.
3. Create an Android Studio Project
Fire up your Android Studio 3 and create a new project with an empty activity called MainActivity
. Make sure to check Include Kotlin support.
4. Set Up Espresso and AndroidJUnitRunner
After creating a new project, make sure to add the following dependencies from the Android Testing Support Library in your build.gradle (although Android Studio has already included them for us). In this tutorial, we are using the latest Espresso library version 3.0.2 (as of this writing).
android { //... defaultConfig { //... testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } //... } dependencies { //... androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:rules:1.0.2' }
We also included the instrumentation runner AndroidJUnitRunner
:
An Instrumentation
that runs JUnit3 and JUnit4 tests against an Android package (application).
Note that Instrumentation
is simply a base class for implementing application instrumentation code.
Turn Off Animation
The synchronisation of Espresso, which doesn't know how to wait for an animation to finish, can cause some tests to fail—if you allow animation on your test device. To turn off animation on your test device, go to Settings > Developer Options and turn off all the following options under the "Drawing" section:
- Window animation scale
- Transition animation scale
- Animator duration scale
5. Write Your First Test in Espresso
First, we start off testing a Login screen. Here's how the login flow starts: the user launches the app, and the first screen shown contains a single Login button. When that Login button is clicked, it opens up the LoginActivity
screen. This screen contains just two EditText
s (the username and password fields) and a Submit button.
Here's what our MainActivity
layout looks like:
Here's what our LoginActivity
layout looks like:
Let's now write a test for our MainActivity
class. Go to your MainActivity
class, move the cursor to the MainActivity
name, and press Shift-Control-T. Select Create New Test... in the popup menu.
Press the OK button, and another dialog shows up. Choose the androidTest directory and click the OK button once more. Note that because we are writing an instrumentation test (Android SDK specific tests), the test cases reside in the androidTest/java folder.
Now, Android Studio has successfully created a test class for us. Above the class name, include this annotation: @RunWith(AndroidJUnit4::class)
.
import android.support.test.runner.AndroidJUnit4 import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class MainActivityTest { }
This annotation signifies that all the tests in this class are Android-specific tests.
Testing Activities
Because we want to test an Activity, we have to do a little setup. We need to inform Espresso which Activity to open or launch before executing and destroy after executing any test method.
import android.support.test.rule.ActivityTestRule import android.support.test.runner.AndroidJUnit4 import org.junit.Rule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class MainActivityTest { @Rule @JvmField var activityRule = ActivityTestRule<MainActivity>( MainActivity::class.java ) }
Note that the @Rule
annotation means that this is a JUnit4 test rule. JUnit4 test rules are run before and after every test method (annotated with @Test
). In our own scenario, we want to launch MainActivity
before every test method and destroy it after.
We also included the @JvmField
Kotlin annotation. This simply instructs the compiler not to generate getters and setters for the property and instead to expose it as a simple Java field.
Here are the three major steps in writing an Espresso test:
- Look for the widget (e.g.
TextView
orButton
) you want to test. - Perform one or more actions on that widget.
- Verify or check to see if that widget is now in a certain state.
The following types of annotations can be applied to the methods used inside the test class.
@BeforeClass
: this indicates that the static method this annotation is applied to must be executed once and before all tests in the class. This could be used, for example, to set up a connection to a database.@Before
: indicates that the method this annotation is attached to must be executed before each test method in the class.@Test
: indicates that the method this annotation is attached to should run as a test case.@After
: indicates that the method this annotation is attached to should run after each test method.@AfterClass
: indicates that the method this annotation is attached to should run after all the test methods in the class have been run. Here, we typically close out resources that were opened in@BeforeClass
.
Find a View
Using onView()
In our MainActivity
layout file, we just have one widget—the Login
button. Let's test a scenario where a user will find that button and click on it.
import android.support.test.espresso.Espresso.onView import android.support.test.espresso.matcher.ViewMatchers.withId // ... @RunWith(AndroidJUnit4::class) class MainActivityTest { // ... @Test @Throws(Exception::class) fun clickLoginButton_opensLoginUi() { onView(withId(R.id.btn_login)) } }
To find widgets in Espresso, we make use of the onView()
static method (instead of findViewById()
). The parameter type we supply to onView()
is a Matcher
. Note that the Matcher
API doesn't come from the Android SDK but instead from the Hamcrest Project. Hamcrest's matcher library is inside the Espresso library we pulled via Gradle.
The onView(withId(R.id.btn_login))
will return a ViewInteraction
that is for a View
whose ID is R.id.btn_login
. In the example above, we used withId()
to look for a widget with a given id. Other view matchers we can use are:
withText()
: returns a matcher that matchesTextView
based on its text property value.withHint()
: returns a matcher that matchesTextView
based on its hint property value.withTagKey()
: returns a matcher that matchesView
based on tag keys.withTagValue()
: returns a matcher that matchesView
s based on tag property values.
First, let's test to see if the button is actually displayed on the screen.
onView(withId(R.id.btn_login)).check(matches(isDisplayed()))
Here, we are just confirming if the button with the given id (R.id.btn_login
) is visible to the user, so we use the check()
method to confirm if the underlying View
has a certain state—in our case, if it is visible.
The matches()
static method returns a generic ViewAssertion
that asserts that a view exists in the view hierarchy and is matched by the given view matcher. That given view matcher is returned by calling isDisplayed()
. As suggested by the method name, isDisplayed()
is a matcher that matches View
s that are currently displayed on the screen to the user. For example, if we want to check if a button is enabled, we simply pass isEnabled()
to matches()
.
Other popular view matchers we can pass into the matches()
method are:
hasFocus()
: returns a matcher that matchesView
s that currently have focus.isChecked()
: returns a matcher that accepts if and only if the view is aCompoundButton
(or subtype of) and is in checked state. The opposite of this method isisNotChecked()
.isSelected()
: returns a matcher that matchesView
s that are selected.
To run the test, you can click the green triangle beside the method or the class name. Clicking the green triangle beside the class name will run all the test methods in that class, while the one beside a method will run the test only for that method.
Hooray! Our test passed!
Perform Actions on a View
On a ViewInteraction
object which is returned by calling onView()
, we can simulate actions a user can perform on a widget. For example, we can simulate a click action by simply calling the click()
static method inside the ViewActions
class. This will return a ViewAction
object for us.
The documentation says that ViewAction
is:
Responsible for performing an interaction on the given View element.
@Test fun clickLoginButton_opensLoginUi() { // ... onView(withId(R.id.btn_login)).perform(click()) }
We perform a click event by first calling perform()
. This method performs the given action(s) on the view selected by the current view matcher. Note that we can pass it a single action or a list of actions (executed in order). Here, we gave it click()
. Other possible actions are:
typeText()
to imitate typing text into anEditText
.clearText()
to simulate clearing text in anEditText
.doubleClick()
to simulate double-clicking aView
.longClick()
to imitate long-clicking aView
.scrollTo()
to simulate scrolling aScrollView
to a particularView
that is visible.swipeLeft()
to simulate swiping right to left across the vertical center of aView
.
Many more simulations can be found inside the ViewActions
class.
Validate With View Assertions
Let's complete our test, to validate that the LoginActivity
screen is shown whenever the Login button is clicked. Though we have already seen how to use check()
on a ViewInteraction
, let's use it again, passing it another ViewAssertion
.
@Test fun clickLoginButton_opensLoginUi() { // ... onView(withId(R.id.tv_login)).check(matches(isDisplayed())) }
Inside the LoginActivity
layout file, apart from EditText
s and a Button
, we also have a TextView
with ID R.id.tv_login
. So we simply do a check to confirm that the TextView
is visible to the user.
Now you can run the test again!
Your tests should pass successfully if you followed all the steps correctly.
Here's what happened during the process of executing our tests:
- Launched the
MainActivity
using theactivityRule
field. - Verified if the Login button (
R.id.btn_login
) was visible (isDisplayed()
) to the user. - Simulated a click action (
click()
) on that button. - Verified if the
LoginActivity
was shown to the user by checking if aTextView
with idR.id.tv_login
in theLoginActivity
is visible.
You can always refer to the Espresso cheat sheet to see the different view matchers, view actions, and view assertions available.
6. Test the LoginActivity
Screen
Here's our LoginActivity.kt:
import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.widget.Button import android.widget.EditText import android.widget.TextView class LoginActivity : AppCompatActivity() { private lateinit var usernameEditText: EditText private lateinit var loginTitleTextView: TextView private lateinit var passwordEditText: EditText private lateinit var submitButton: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) usernameEditText = findViewById(R.id.et_username) passwordEditText = findViewById(R.id.et_password) submitButton = findViewById(R.id.btn_submit) loginTitleTextView = findViewById(R.id.tv_login) submitButton.setOnClickListener { if (usernameEditText.text.toString() == "chike" && passwordEditText.text.toString() == "password") { loginTitleTextView.text = "Success" } else { loginTitleTextView.text = "Failure" } } } }
In the code above, if the entered username is "chike" and the password is "password", then the login is successful. For any other input, it's a failure. Let's now write an Espresso test for this!
Go to LoginActivity.kt, move the cursor to the LoginActivity
name, and press Shift-Control-T. Select Create New Test... in the popup menu. Follow the same process as we did for MainActivity.kt, and click the OK button.
import android.support.test.espresso.Espresso import android.support.test.espresso.Espresso.onView import android.support.test.espresso.action.ViewActions import android.support.test.espresso.assertion.ViewAssertions.matches import android.support.test.espresso.matcher.ViewMatchers.withId import android.support.test.espresso.matcher.ViewMatchers.withText import android.support.test.rule.ActivityTestRule import android.support.test.runner.AndroidJUnit4 import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LoginActivityTest { @Rule @JvmField var activityRule = ActivityTestRule<LoginActivity>( LoginActivity::class.java ) private val username = "chike" private val password = "password" @Test fun clickLoginButton_opensLoginUi() { onView(withId(R.id.et_username)).perform(ViewActions.typeText(username)) onView(withId(R.id.et_password)).perform(ViewActions.typeText(password)) onView(withId(R.id.btn_submit)).perform(ViewActions.scrollTo(), ViewActions.click()) Espresso.onView(withId(R.id.tv_login)) .check(matches(withText("Success"))) } }
This test class is very similar to our first one. If we run the test, our LoginActivity
screen is opened. The username and password are typed into the R.id.et_username
and R.id.et_password
fields respectively. Next, Espresso will click the Submit button (R.id.btn_submit
). It will wait until a View
with id R.id.tv_login
can be found with text reading Success.
7. Test a RecyclerView
RecyclerViewActions
is the class that exposes a set of APIs to operate on a RecyclerView
. RecyclerViewActions
is part of a separate artifact inside the espresso-contrib
artifact, which also should be added to build.gradle:
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
Note that this artifact also contains the API for UI testing the navigation drawer through DrawerActions
and DrawerMatchers
.
@RunWith(AndroidJUnit4::class) class MyListActivityTest { // ... @Test fun clickItem() { onView(withId(R.id.rv)) .perform(RecyclerViewActions .actionOnItemAtPosition<RandomAdapter.ViewHolder>(0, ViewActions.click())) } }
To click on an item at any position in a RecyclerView
, we invoke actionOnItemAtPosition()
. We have to give it a type of item. In our case, the item is the ViewHolder
class inside our RandomAdapter
. This method also takes in two parameters; the first is the position, and the second is the action (ViewActions.click()
).
Other RecyclerViewActions
that can be performed are:
actionOnHolderItem()
: performs aViewAction
on a view matched byviewHolderMatcher
. This allows us to match it by what's contained inside theViewHolder
rather than the position.scrollToPosition()
: returns aViewAction
which scrollsRecyclerView
to a position.
Next (once the "add note screen" is open), we will enter our note text and save the note. We don't need to wait for the new screen to open—Espresso will do this automatically for us. It waits until a View with the id R.id.add_note_title
can be found.
8. Test Intents
Espresso makes use of another artifact named espresso-intents
for testing intents. This artifact is just another extension to Espresso that focuses on the validation and mocking of Intents. Let's look at an example.
First, we have to pull the espresso-intents
library into our project.
androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.2'
import android.support.test.espresso.intent.rule.IntentsTestRule import android.support.test.runner.AndroidJUnit4 import org.junit.Rule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PickContactActivityTest { @Rule @JvmField var intentRule = IntentsTestRule<PickContactActivity>( PickContactActivity::class.java ) }
IntentsTestRule
extends ActivityTestRule
, so they both have similar behaviours. Here's what the doc says:
This class is an extension ofActivityTestRule
, which initializes Espresso-Intents before each test annotated withTest
and releases Espresso-Intents after each test run. The Activity will be terminated after each test and this rule can be used in the same way asActivityTestRule
.
The main differentiating feature is that it has additional functionalities for testing startActivity()
and startActivityForResult()
with mocks and stubs.
We are now going to test a scenario where a user will click on a button (R.id.btn_select_contact
) on the screen to pick a contact from the phone's contact list.
// ... @Test fun stubPick() { var result = Instrumentation.ActivityResult(Activity.RESULT_OK, Intent(null, ContactsContract.Contacts.CONTENT_URI)) intending(hasAction(Intent.ACTION_PICK)).respondWith(result) onView(withId(R.id.btn_select_contact)).perform(click()) intended(allOf( toPackage("com.google.android.contacts"), hasAction(Intent.ACTION_PICK), hasData(ContactsContract.Contacts.CONTENT_URI))) //... }
Here we are using intending()
from the espresso-intents
library to set up a stub with a mock response for our ACTION_PICK
request. Here's what happens in PickContactActivity.kt when the user clicks the button with id R.id.btn_select_contact
to pick a contact.
fun pickContact(v: View) val i = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI) startActivityForResult(i, PICK_REQUEST) }
intending()
takes in a Matcher
that matches intents for which stubbed response should be provided. In other words, the Matcher
identifies which request you're interested in stubbing. In our own case, we make use of hasAction()
(a helper method in IntentMatchers
) to find our ACTION_PICK
request. We then invoke respondWith()
, which sets the result for onActivityResult()
. In our case, the result has Activity.RESULT_OK
, simulating the user selecting a contact from the list.
We then simulate clicking the select contact button, which calls startActivityForResult()
. Note that our stub sent the mock response to onActivityResult()
.
Finally, we use the intended()
helper method to simply validate that the calls to startActivity()
and startActivityForResult()
were made with the right information.
Conclusion
In this tutorial, you learned how to easily use the Espresso testing framework in your Android Studio project to automate your test workflow.
I highly recommend checking out the official documentation to learn more about writing UI tests with Espresso.