Using Espresso to Test and Stub Android Intents
In modern application development, a user flow frequently involves interaction with other applications on the device. In Android, the most universal example of this interaction is when a user selects data and clicks the share button. On the surface, this feature seems straightforward, but the Android OS always prioritizes user privacy, and therefore implementation requires multifaceted considerations. To interact with another application, your application will have to send an Intent
to the Android System, which in turn starts another application's activity based on the specification provided in the Intent
. The Espresso framework, which we discussed in a previous article, exposes an extension called espresso-intents
to validate user flows involving an Intent
.
This article aims to familiarize the reader with Intent
creation, their stubbing, and testing user flows involving them. To accomplish this, we will create an application with an Intent
to interact with another application. In addition, we will outline the espresso-intents
API. Finally, we will create an instrumented test using Espresso to validate this user flow.
Create an Android application that reads Contacts
To create a blank application, open Android Studio.
- Click + Create New Project > Empty Activity > Next
- Select Name and click Finish
After the initial Gradle sync finishes, Android Studio will open MainActivity.java
in the editor. Close this file for now. Open the src/main/res/layout/activity_main.xml
file. Using this file we can quickly create the layout for our basic application.
Create the layout
Copy the below code to activity_main.xml
file that you just opened.
This file creates the layout for your application’s MainActivity
and consists of a TextView
and a Button
. Whenever you click on the contact_select
button, you are redirected to a Contact Picker. When you pick a contact from the list of contacts, you are redirected back to the original page. This page now displays the phone number of the contact you just picked in the contact_phone_number
view. In essence, when you clicked the contact_select
button, an Intent
to pick a contact from the Contacts application got initialized.
You have likely noticed that Android Studio may be complaining about missing symbols and missing handlers after pasting the code. Let’s add the missing symbols first. Open the file at src/main/res/values/strings.xml
and paste the following code. This should resolve the missing symbols issue. Keeping code separate from hardcoded strings is a good practice to reduce redundancies and bugs.
To resolve the issue of missing handlers, we will need to open the MainActivity.java
again. We will now add an onClick
handler for the contact_select
button. However, before we move on take a look at this user interface that was created by the above XML layout file.
Note: I created the simulator using an automated script that I described in this article.
Add onClick Handler to Button
The MainActivity.java
file contains the application code and navigation logic. I have added additional comments to create markers for additional code. Let's go ahead and add an onClick handler.
Note: Do not change the name of the onClick method as it has been already entered in the layout file.
To add an onClick handler, paste the following code in the MainActivity.java
The onClick method has a parameter called view
which is the event's target. In this case, view
would be the contact_select
button reference. As you can see, when this button is clicked we initialize a new Intent
and give it an action called ACTION_PICK
. Intents allow you to create a facility to bind the code of different applications at runtime. In essence, intents bridge applications and allow the Android System to preserve the user's privacy. The android documentation states that this action will:
Pick an item from the data, returning what was selected.
To further filter the type of data, we set the data type from the Contacts API as ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE
which is the type definition for a phone number. Then, we use an ActivityResultLauncher getPhoneNumber
to launch the contact picker activity. In the next section, we will add the code for this launcher.
Initialize an ActivityResultLauncher
Paste this ActivityResultLauncher
code snippet in your MainActivity.java
The MainActivity
class extends AppCompatActivity
which has access to a global called registerForActivityResult
. This method returns a launcher which you will use to launch another activity.
registerForActivityResult
takes anActivityResultsContract
as a parameter.ActivityResultsContract
is an interface to manage intent creation and result parsing. There are many prebuilt contracts, but I chose to go with explicitly creating my own intent for the purpose of testing them and parsing its results.- The second parameter is a lambda function. It takes in the result of the activity which was launched and destroyed and processes it. In our case, we will parse the results for the phone number and assign it to the
contact_phone_number
view. The launcher returns an Intent with data. A URI is part of the data. A URI is an information scheme used in Android to get application data from other applications. In our case, we are accessing the data from the Contacts application. We do so using aContentResolver
which allows us to query the returned URI. All this code is abstracted in thegetPhoneNumberFromUri()
helper method that we will discuss in the next section.
Parse the Activity Result Intent
The following code should also go in your MainActivity.java
file
The method query
returns a cursor which allows us to access the data returned.
- We pass it the Content URI and a projection of what columns of data we are looking for. In our case, we are only concerned with the phone number. The projection is a
String[]
. - We move to the first row available to the returned cursor. This is done using
cursor.moveToFirst();
call. - We get the index of the column which has a phone number type. The method responsible for this is
getColumnIndex(Type)
- We get the value of this column and return this phone number. not it is necessary to close opened
cursors
to avoid memory leaks and crashes.
If you run this application, depending on which API you are running, it can crash. This is because we need to specify that our application needs permission to read contacts in the user’s device. To do this, add <uses-permission android:name="android.permission.READ_CONTACTS" />
above the application tag in the AndroidManifest.xml
file.
Let go ahead and run the application and click on the Select a Contact button. Click on one of the contacts and you will see that the contact’s phone number is displayed in the text view. This is a very simple use case that has similarities to many complicated counterparts.
Outline the espresso-intents
API
The extension provided allows you to validate and stub intents in your application. Quite literally, it exposes two methods: intended()
for validation, and intending()
for stubbing.
intended()
This method takes matchers as input to verify that the intent that got triggered by your application had the proper structure, actions, and data. It also verifies the outgoing package and the number of intents that were triggered. All Intents that get triggered after the Intents.init()
call are recorded for verification. The IntentMatchers reference in the Android documentation provides a list of all available matchers that can help validate the intent structure.
intending()
This method takes matchers as input. This mocking is especially useful for hermetic testing to verify your application’s instrumentation in isolation. This can help your testing process become independent of external applications. For example, if you launch an activity in your app and expect a result, you can use this for testing. This method will allow you to send out an Intent ad return the expected Activity result without ever launching the activity
Add Instrumented tests using espresso-intents
The above file contains two instrumented tests: to validate intents and to stub intents. The following bullet points will elaborate on the code snippet
IntentsTestRule
inherits fromActivityTestRule
and abstracts away boilerplate code of initializing and releasing Intents before and after each test.GrantPermissionRule
allows the espresso test to read contacts.validatePickContactIntent
finds thecontact_select
button and clicks it. Since intents were initialized before this test began, all outgoing intents will be recorded including ourACTION_PICK
intent. We capture this intent usingintended
and validate it using the two matchers two verify its actions and data type of result.stubPickContactIntent
creates a new intent similar to the one inMainActivity.java
. Any intent with actionACTION_PICK
will return the mockedActivityResult
getContactUriByPhoneNumber
takes aString gPhoneNumber
and searches the contacts for this phone number. It then returns the proper encoded URI pointing to the contact with this phone number.
Conclusion
As you can see, with just a few lines of code, we were able to not only validate intents but also stub them to check our application code in isolation. Espresso also provides other extensions to test WebViews and testing IdlingResources
which might be discussed in a future article. Stay Tuned!