Android Intent is used to open new activity, either internal (opening a product detail screen from product list screen) or external (like opening a dialer to make a call). Internal intent activity is handled transparently by the espresso testing framework and it does not need any specific work from the user side. However, invoking external activity is really a challenge because it goes out of our scope, the application under test. Once the user invokes an external application and goes out of the application under test, then the chances of user coming back to the application with predefined sequence of action is rather less. Therefore, we need to assume the user action before testing the application. Espresso provides two options to handle this situation. They are as follows,
This allows the user to make sure the correct intent is opened from the application under test.
This allows the user to mock an external activity like take a photo from the camera, dialing a number from the contact list, etc., and return to the application with predefined set of values (like predefined image from the camera instead of actual image).
Espresso supports the intent option through a plugin library and the library needs to be configured in the application’s gradle file. The configuration option is as follows,
dependencies { // ... androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1' }
Espresso intent plugin provides special matchers to check whether the invoked intent is the expected intent. The provided matchers and the purpose of the matchers are as follows,
This accepts the intent action and returns a matcher, which matches the specified intent.
This accepts the data and returns a matcher, which matches the data provided to the intent while invoking it.
This accepts the intent package name and returns a matcher, which matches the package name of the invoked intent.
Now, let us create a new application and test the application for external activity using intended() to understand the concept.
Start Android studio.
Create a new project as discussed earlier and name it, IntentSampleApp.
Migrate the application to AndroidX framework using Refactor → Migrate to AndroidX option menu.
Create a text box, a button to open contact list and another one to dial a call by changing the activity_main.xml as shown below,
<?xml version = "1.0" encoding = "utf-8"?> <RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android" xmlns:app = "http://schemas.android.com/apk/res-auto" xmlns:tools = "http://schemas.android.com/tools" android:layout_width = "match_parent" android:layout_height = "match_parent" tools:context = ".MainActivity"> <EditText android:id = "@+id/edit_text_phone_number" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_centerHorizontal = "true" android:text = "" android:autofillHints = "@string/phone_number"/> <Button android:id = "@+id/call_contact_button" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_centerHorizontal = "true" android:layout_below = "@id/edit_text_phone_number" android:text = "@string/call_contact"/> <Button android:id = "@+id/button" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_centerHorizontal = "true" android:layout_below = "@id/call_contact_button" android:text = "@string/call"/> </RelativeLayout>
Also, add the below item in strings.xml resource file,
<string name = "phone_number">Phone number</string> <string name = "call">Call</string> <string name = "call_contact">Select from contact list</string>
Now, add the below code in the main activity (MainActivity.java) under the onCreate method.
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { // ... code // Find call from contact button Button contactButton = (Button) findViewById(R.id.call_contact_button); contactButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // Uri uri = Uri.parse("content://contacts"); Intent contactIntent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); contactIntent.setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE); startActivityForResult(contactIntent, REQUEST_CODE); } }); // Find edit view final EditText phoneNumberEditView = (EditText) findViewById(R.id.edit_text_phone_number); // Find call button Button button = (Button) findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if(phoneNumberEditView.getText() != null) { Uri number = Uri.parse("tel:" + phoneNumberEditView.getText()); Intent callIntent = new Intent(Intent.ACTION_DIAL, number); startActivity(callIntent); } } }); } // ... code }
Here, we have programmed the button with id, call_contact_button to open the contact list and button with id, button to dial the call.
Add a static variable REQUEST_CODE in MainActivity class as shown below,
public class MainActivity extends AppCompatActivity { // ... private static final int REQUEST_CODE = 1; // ... }
Now, add the onActivityResult method in the MainActivity class as below,
public class MainActivity extends AppCompatActivity { // ... @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE) { if (resultCode == RESULT_OK) { // Bundle extras = data.getExtras(); // String phoneNumber = extras.get("data").toString(); Uri uri = data.getData(); Log.e("ACT_RES", uri.toString()); String[] projection = { ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME }; Cursor cursor = getContentResolver().query(uri, projection, null, null, null); cursor.moveToFirst(); int numberColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER); String number = cursor.getString(numberColumnIndex); int nameColumnIndex = cursor.getColumnIndex( ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME); String name = cursor.getString(nameColumnIndex); Log.d("MAIN_ACTIVITY", "Selected number : " + number +" , name : "+name); // Find edit view final EditText phoneNumberEditView = (EditText) findViewById(R.id.edit_text_phone_number); phoneNumberEditView.setText(number); } } }; // ... }
Here, onActivityResult will be invoked when a user returns to the application after opening the contact list using the call_contact_button button and selecting a contact. Once the onActivityResult method is invoked, it gets the user selected contact, find the contact number and set it into the text box.
Run the application and make sure everything is fine. The final look of the Intent sample Application is as shown below,
Now, configure the espresso intent in the application’s gradle file as shown below,
dependencies { // ... androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1' }
Click the Sync Now menu option provided by the Android Studio. This will download the intent test library and configure it properly.
Open ExampleInstrumentedTest.java file and add the IntentsTestRule instead of normally used AndroidTestRule. IntentTestRule is a special rule to handle intent testing.
public class ExampleInstrumentedTest { // ... code @Rule public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(MainActivity.class); // ... code }
Add two local variables to set the test phone number and dialer package name as below,
public class ExampleInstrumentedTest { // ... code private static final String PHONE_NUMBER = "1 234-567-890"; private static final String DIALER_PACKAGE_NAME = "com.google.android.dialer"; // ... code }
Fix the import issues by using Alt + Enter option provided by android studio or else include the below import statements,
import android.content.Context; import android.content.Intent; import androidx.test.InstrumentationRegistry; import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.runner.AndroidJUnit4; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.intent.Intents.intended; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData; import static androidx.test.espresso.intent.matcher.IntentMatchers.toPackage; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static org.hamcrest.core.AllOf.allOf; import static org.junit.Assert.*;
Add the below test case to test whether the dialer is properly called,
public class ExampleInstrumentedTest { // ... code @Test public void validateIntentTest() { onView(withId(R.id.edit_text_phone_number)) .perform(typeText(PHONE_NUMBER), closeSoftKeyboard()); onView(withId(R.id.button)) .perform(click()); intended(allOf( hasAction(Intent.ACTION_DIAL), hasData("tel:" + PHONE_NUMBER), toPackage(DIALER_PACKAGE_NAME))); } // ... code }
Here, hasAction, hasData and toPackage matchers are used along with allOf matcher to succeed only if all matchers are passed.
Now, run the ExampleInstrumentedTest through content menu in Android studio.
Espresso provides a special method – intending() to mock an external intent action. intending() accept the package name of the intent to be mocked and provides a method respondWith to set how the mocked intent needs to be responded with as specified below,
intending(toPackage("com.android.contacts")).respondWith(result);
Here, respondWith() accepts intent result of type Instrumentation.ActivityResult. We can create new stub intent and manually set the result as specified below,
// Stub intent Intent intent = new Intent(); intent.setData(Uri.parse("content://com.android.contacts/data/1")); Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, intent);
The complete code to test whether a contact application is properly opened is as follows,
@Test public void stubIntentTest() { // Stub intent Intent intent = new Intent(); intent.setData(Uri.parse("content://com.android.contacts/data/1")); Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, intent); intending(toPackage("com.android.contacts")).respondWith(result); // find the button and perform click action onView(withId(R.id.call_contact_button)).perform(click()); // get context Context targetContext2 = InstrumentationRegistry.getInstrumentation().getTargetContext(); // get phone number String[] projection = { ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME }; Cursor cursor = targetContext2.getContentResolver().query(Uri.parse("content://com.android.cont acts/data/1"), projection, null, null, null); cursor.moveToFirst(); int numberColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER); String number = cursor.getString(numberColumnIndex); // now, check the data onView(withId(R.id.edit_text_phone_number)) .check(matches(withText(number))); }
Here, we have created a new intent and set the return value (when invoking the intent) as the first entry of the contact list, content://com.android.contacts/data/1. Then we have set the intending method to mock the newly created intent in place of contact list. It sets and calls our newly created intent when the package, com.android.contacts is invoked and the default first entry of the list is returned. Then, we fired the click() action to start the mock intent and finally checks whether the phone number from invoking the mock intent and number of the first entry in the contact list are same.
It there is any missing import issue, then fix those import issues by using Alt + Enter option provided by android studio or else include the below import statements,
import android.app.Activity; import android.app.Instrumentation; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract; import androidx.test.InstrumentationRegistry; import androidx.test.espresso.ViewInteraction; import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.runner.AndroidJUnit4; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.intent.Intents.intended; import static androidx.test.espresso.intent.Intents.intending; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData; import static androidx.test.espresso.intent.matcher.IntentMatchers.toPackage; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.core.AllOf.allOf; import static org.junit.Assert.*;
Add the below rule in the test class to provide permission to read contact list −
@Rule public GrantPermissionRule permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS);
Add the below option in the application manifest file, AndroidManifest.xml −
<uses-permission android:name = "android.permission.READ_CONTACTS" />
Now, make sure the contact list has at least one entry and then run the test using context menu of the Android Studio.