Using Espresso to Test and Stub Android Intents

Ranvir Chhina
Tauk Blog
Published in
7 min readNov 8, 2021

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.

  1. Click + Create New Project > Empty Activity > Next
  2. 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 an ActivityResultsContract 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 a ContentResolver which allows us to query the returned URI. All this code is abstracted in the getPhoneNumberFromUri() 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 from ActivityTestRule 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 the contact_select button and clicks it. Since intents were initialized before this test began, all outgoing intents will be recorded including our ACTION_PICK intent. We capture this intent using intended 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 in MainActivity.java. Any intent with action ACTION_PICK will return the mocked ActivityResult
  • getContactUriByPhoneNumber takes a String 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!

--

--