Android Espresso Test for Intent

Intent Test

An intent is an abstract description of an operation to be performed.

You all know Intent, and you also know it works with startActivity or startActivityForResult to start another activity or app to get result. How to test these behaviours in the UI test is a little tricky, because we cannot manipulate the user interface of an external activity nor control the ActivityResult returned to the activity under test. Luckily, Espresso has a nice package Intents, which handles this problem. There are three situations you need to think about for the Intent test and I will describe an example for each situation.

  1. Sending the user to another App
  2. Getting a Result from an Activity
  3. Allowing Other Apps to start your Activity

Setup dependence

The Intentspackage is not in the default Espresso package, you need to add it with other Espresso packages: the core, the rules, and the runner:

androidTestCompile 'com.android.support.test:runner:0.4.1'
androidTestCompile 'com.android.support.test:rules:0.4.1'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1'

External Intent

External intent is used to send user to another app to get result: such as open camera to take a picture or open contact to select an email.

Here is an example: user select an image from a gallery app, then your app displays the selected image in an ImageView. The Intent to trigger the event will be something like this:

Intent intent = new Intent(Intent.ACTION_PICK,
               android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, MainActivity.RESULT_LOAD_IMAGE);

What you want to test in your onActivityResult is you receive a validated image Uri (the selected image is parsed as Uri), you can display it in the ImageView. You can’t control which app user use to select the image, as long as it will return a validated Uri in the result.

First, let’s use Instrumentation to create a ActivityResult. Here I use launch icon as test image and parse it as an Uri.

Resources resources = InstrumentationRegistry.getTargetContext().getResources();
 Uri imageUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + 
 		resources.getResourcePackageName(R.mipmap.ic_launcher) + '/' + 
                resources.getResourceTypeName(R.mipmap.ic_launcher) + '/' + 
                resources.getResourceEntryName(R.mipmap.ic_launcher));
        
Intent resultData = new Intent();
resultData.setData(imageUri);
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(
                Activity.RESULT_OK, resultData);

There are two methods you need to understand when using Intents: intending and intended. If you use Mockito, they are very similar.

  • intending is like when and respondWith is like thenReturn
  • intended is like verify, assert that a given intent has been seen

Before you can use these two methods, you need to initialize the Intents, which is not clear when I first use the Intents, otherwise you will get a NullPointerException Exception.

In the Mockito, you will use method name to match the action. In the Intents, you need to use Matcher<Intent> to match the Intent. In the existing intent matcher, it has many methods responding to the Setter you can use for the Intent:

  • Intent.setData <–> hasData
  • Intent.setAction <–> hasAction
  • Intent.setFlag <–> hasFlag
  • Intent.setComponent <–> hasComponent

Here is the code to match the image select Intent:

Matcher<Intent> expectedIntent = allOf(hasAction(Intent.ACTION_PICK),
       hasData(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI));
Intents.init();
intending(expectedIntent).respondWith(result);

 //Click the select button
onView(withId(R.id.fab_image)).perform(click());
intended(expectedIntent);
Intents.release();

//Check the image is displayed
onView(withId(R.id.imageView)).check(matches(hasDrawable()));

If you want to implement your own intent matcher, you can check this tutorial

Internal Intent

Internal Intent is used to getting a result from an Activity in the same app: such as login, select an item.

This situation is easy to test because you control the interaction, but you need to consider all the situation such as user cancel the action (press back button).

Here is an example to test what happen if user cancel the login action.

Instrumentation.ActivityResult activityResult = new Instrumentation.ActivityResult(
                Activity.RESULT_CANCELED, new Intent());

Intents.init();
intending(expectedIntent).respondWith(activityResult);
onView(withId(R.id.button_login)).perform(click());
intended(expectedIntent);
Intents.release();

onView(withId(R.id.button_login)).check(matches(withText(R.string.cancel_login)));

Share Intent

Allowing other Apps to Start your app and perform an action that might be useful to another app

There are two parts you want to test:

  1. You app responses to the correct Intent
  2. You app does the correct action when receive the validated incoming data

Caution: Take extra care to check the incoming data, you never know what other application may send you.

// Setup the trigger Intent
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setType(TextHashActivity.SUPPORT_TYPE);
intent.putExtra(Intent.EXTRA_TEXT, TEST_INPUT);

// Need to set the Intent explicit here, because it may have more than one app to handle this action and we don't want to have the App chooser here

String packageName = InstrumentationRegistry.getTargetContext().getPackageName();
ComponentName componentName = new ComponentName(packageName,
                TextHashActivity.class.getName());
intent.setComponent(componentName);

Intents.init();
// start activity from test app
InstrumentationRegistry.getContext().startActivity(intent);

// Check the intent is handled by the app
Matcher<Intent> expectedIntent = hasComponent(componentName);
intended(expectedIntent);
Intents.release();

// Check the action is correctly performed 
onView(withId(R.id.textview_sha1))
                .check(matches(withText(TEST_SHA1)));

Sample Source code:

You can find the sample source code in Github. The sample app has following actions:

  1. Pick up an image from gallery app and display in an ImageView
  2. Login with user name and password (using android studio Login Activity)
  3. User input some text and display SHA1 value of the text
  4. Another app send some text and display SHA1 value of the text

Read More:

If you want to know more about Espresso Intent test, these post and sample code help me out a lot when writing this post:

  1. Testing-intents-with-espresso-intents
  2. Stub-your-android-intents
  3. Espresso sample
  4. Espresso Intent sample

Missing the back button in iOS 9?

Back Button?

iOS 9 and Swift 2 are awesome, but your app may not work as you expect when running on iOS 9 device. Here is a problem I found.

When user click the edit button, it will bring up a new ViewController, so use can edit data then return to previous screen. It is a simple case to use show (push) segue. Here is the storyboard for the relationship between ViewController.

Here is the screenshot for the app LogICU when running on iOS 8 and 9.

There is no back button in iOS 9, so user can’t go back to the previous screen. When I went to google the problem, someone already report it the here Openradar iOS 9.0 beta 3

Solution

It turns out to fix the problem is very simple: just remove the NavigationViewController from the data edit ViewController. In the iOS 9, you don’t need to integrate your ViewController into a NavigationViewController if your previous ViewController is already integrated with one. Here is the updated storyboard.

For future

  1. Always test the app before the new iOS final version release
  2. Use the new xcode 7 UI test in the future to detect this problem, some nice tuturial from Joe Masilotti [1] [2]
  3. Aware the issues reported by other developers, such as openradar, developer forums