© Denys Zelenchuk 2019
Denys ZelenchukAndroid Espresso Revealedhttps://doi.org/10.1007/978-1-4842-4315-2_5

5. Verifying and Stubbing Intents with IntentMatchers

Denys Zelenchuk1 
(1)
Zürich, Switzerland
 
Throughout this chapter, we will discuss how to verify and stub application intents. An intent is a messaging object you can use to request an action from another app component. Intents facilitate communication between components in several ways. According to the Android Intent and Filters documentation ( https://developer.android.com/guide/components/intents-filters ), there are three fundamental use cases:
  • Starting an activity—An activity represents a single screen in the Android application. An activity instance can be launched by passing an intent to Context.startActivity(Intent). Passed intents should contain information about which activity will be started and may contain extra data. The Context.startActivityForResult(Intent) method is used when we expect to receive the result from a launched activity. The result is returned in the form of an intent object and can be handled in an Activity.onActivityResult() callback.

  • Starting a service—A service in Android represents a mechanism that performs operations in the background. Similar to an activity, a service is started by passing an intent to Context.startService(Intent). Provided intents define the service to start and may contain extra data.

  • Delivering a broadcast —A broadcast represents a message that can be sent and received by any application or system. An example of a system broadcast can be a system bootup event. Broadcasts can be delivered to other apps by passing an intent to Context.sendBroadcast(Intent).

Here are examples of intents that belong to these intent types:
  • Starting an activity intent—Usually an intent to start an activity for a result. An example can be clicking the attachment button in Gmail, which opens the file browser so you can find and attach a file to the email.

  • Starting a service intent—Used to trigger long-lasting processes that are running in the background, like file downloads or for listening for some system events like connectivity state changes.

  • Delivering a broadcast—Used when there is a need to send a local intent, meaning that we would like to broadcast to receivers that are in the same app as the sender. Or just send our broadcast to all apps in the system that can handle it. An example is a broadcast to send an SMS.

As you may already know, Espresso cannot operate outside of the application being tested, which is the common case in starting an activity intent or delivering a broadcast. Therefore, to make Espresso tests isolated and hermetic, we need to use Espresso-Intents, which is an extension to Espresso that enables validation and stubbing of intents sent out by the application being tested.

Setting Up Dependencies

In order to use Espresso-Intents, the following line of code should be added inside the build.gradle file of your app module:

Android Testing Support Library Espresso-Intents Dependency.
androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.2'
AndroidX Test Library Espresso-Intents Dependency.
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'

Note

Espresso-Intents is only compatible with Espresso 2.1+ and the Testing Support library 0.3+ or AndroidX Test library.

So, to fulfill this compatibility requirement, the following dependencies must be updated as well.

Android Testing Support Library Dependencies.
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

Or in case of AndroidX Test library usage, we need the following.

AndroidX Test library Dependencies.
androidTestImplementation 'androidx.test:runner:1.1.0'
androidTestImplementation 'androidx.test:rules:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'

In Chapter 1, we discussed the purpose and role of ActivityTestRule in Espresso tests. Similar to ActivityTestRule, Espresso has the IntentsTestRule, which is the extension of ActivityTestRule and must be used when intents should be stubbed or validated. As in the case of ActivityTestRule, an IntentsTestRule initializes Espresso-Intents before each test is annotated with @Test and releases Espresso-Intents after each test run.

Here is an IntentsTestRule example:
@get:Rule
var intentsTestRule = IntentsTestRule(TasksActivity::class.java)
Our sample application contains functionality for attaching an image to the TO-DO item and is an example of an activity for a result intent that receives an image file from the system. Figure 5-1 shows the intent flow when the start activity intent is sent to a third-party application.
../images/469090_1_En_5_Chapter/469090_1_En_5_Fig1_HTML.jpg
Figure 5-1

Activity intent flow (image source: https://developer.android.com/guide/components/intents-filters )

Step 1 demonstrates sending a start activity intent from our application to notify the system about the need to delegate some functionality to a third-party application. In its order, the system knows what application(s) can be sent in Step 1 and, if at least one application is found, it retransmits the same activity intent to it, which is shown in Step 2. In Step 3, the selected application receives the intent and starts the appropriate activity. In case of an intent that is sent with startActivityForResult() , the result of the started activity (for example, the selected image link from the Gallery or Photos application) is returned to the application that initially created the intent.

It is a time to look at how Espresso stubs intents sent to the third-party applications outside of the application context.

Stubbing Activity Intents

As mentioned, Espresso does not support leaving applications under the text context, i.e. leaving the tested application, in order to interact with third-party applications. For this reason, Espresso provides the stubbing mechanism intending() method in the Intents class .

This method enables stubbing intent responses and is particularly useful when the activity launching the intent expects data to be returned (and especially when the destination activity is external). In this case, the test author can call:
intending(intentMatcher).thenRespond(myResponse)

and validate that the launching activity handles the result correctly.

Note

The third-party application destination activity will not be launched in this code sample.

Stubbing Intents Without a Result

The first use case with intent stubbing can isolate our application from any action that can lead to the state when a third-party application is launched. To achieve this, the Espresso intending() mechanism enables stubbing intents that are not internal, i.e., that do not belong to our application. Here is how it can be implemented in the test class @Before method .

chapter5.StubAllIntentsTest.kt.
@Before
fun stubAllExternalIntents() {
    // By default Espresso Intents does not stub any Intents. Stubbing needs to be setup before
    // every test run. In this case all external Intents will be blocked.
    intending(not(isInternal()))
            .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
}

Note

The method annotated with the @Before annotation will be executed for every test case before it’s run.

You can observe two new methods in this code example:
  • isInternal() Intent matcher that matches an intent if its package is the same as the target package for the instrumentation test.

  • Instrumentation.ActivityResult(Activity.RESULT_OK, null) —The ActivityResult class that allows us to create a new ActivityResult, which will be propagated back to the original activity with the specified result code. See the Android Instrumentation.java class and the Activity.setResult() method for more details.

We are already familiar with hamcrest matchers from Chapters 1 and 2. IntentMatchers have a similar functionality. Along with intent matchers, Espresso provides BundleMatchers, ComponentNameMatchers, and UriMatchers, which are used together with IntentMatchers. Here is a brief overview of all of them.

IntentMatchers :
  • anyIntent()—Matches any intent.

  • hasAction()—Matches an intent by intent action. The most common example is Intent.ACTION_CALL to perform the phone call action or Intent.ACTION_SEND to send an email or SMS. For more action types, refer to the Android Intent.java class.

  • hasCategories()—Matches an intent category, which is the string containing additional information about the kind of component that should handle the intent. For example, the string value for CATEGORY_LAUNCHER is android.intent.category.LAUNCHER and is used to specify the initial application activity.

  • hasComponent()—Can match an intent by class name, package name, or short class name. Uses ComponentNameMatchers.

  • hasData()—Matches an intent that has specific data this intent is operating on. Often it uses the content: scheme, specifying data in a content provider. Other schemes may be handled by specific activities, such as http: by the web browser. Uses UriMatchers.

  • hasExtraWithKey()—Matches an intent that has specific bundle attached to the intent. Uses a hasExtras() method that takes the bundle matcher as a parameter.

  • hasExtra()—Same as hasExtras() but with extra data.

  • hasExtras()—Matches an intent that has specific extended or extra data. This data is put into the intent in the form of a <name, value> pair by one of the overloaded Intent.putExtra() methods. The name of the extra parameter must include a package prefix. For example, the app com.android.contacts would use names like com.android.contacts.ShowAll.

  • hasType()—Matches an intent with the explicit MIME type included in it.

  • hasPackage()—Matches an intent that is limited to a specified application package name.

  • toPackage()⎯Matches an intent based on the package of activity that can handle the intent.

  • hasFlag()⎯Same as getFlags() .

  • hasFlags()⎯Matches an intent with specified flag(s) associated with it. The list of flags can be found at https://developer.android.com/reference/android/content/Intent#setFlags(int) .

  • isInternal()⎯Matches an intent if its package is the same as the target package for the instrumentation test.

The BundleMatchers class represents hamcrest matchers for intent bundles. Bundles are used for passing data between activities, usually in form of a <key, value> pair.
  • hasEntry()⎯Matches a bundle object based on a <key, value> pair.

  • hasKey()⎯Matches a bundle object based on a key.

  • hasValue()⎯Matches a bundle object based on a value.

ComponentNameMatchers :
  • hasClassName()⎯Matches a component based on a class name.

  • hasPackageName()⎯Matches a component based on a provided package name.

  • hasShortClassName()⎯Matches a component based on the short class name.

  • hasMyPackageName()⎯Matches a component based on the target package name found through the Instrumentation Registry for the test.

UriMatchers ⎯used for matching intents based on the URI object. For example, if the action is ACTION_EDIT, the data should contain the URI of the document to edit.
  • hasHost()⎯Matches the URI object based on the host. For example, if the authority is "[email protected]", this method will try to match the object based on " google.com ".

  • hasParamWithName()⎯Matches the URI object based on the parameter name.

  • hasParamWithValue()⎯Matches the URI object based on the parameter value.

  • hasPath()⎯Matches the URI object based on the path. Like mailto:[email protected].

  • hasSchemeSpecificPart()⎯Matches the URI object based on the specific scheme part. This is everything between the scheme separator ':' and the fragment separator '#'. If this is a relative URI, this method returns the entire URI. For example, "// www.google.com/search?q=android ".

Now let’s return to the chapter5.StubAllIntentsTest.kt class and see how intent stubbing works. Here is the class implementation.

chapter5.StubAllIntentsTest.kt.
class StubAllIntents {
    @get:Rule
    var intentsTestRule = IntentsTestRule(TasksActivity::class.java)
    private var toDoTitle = ""
    private var toDoDescription = ""
    // ViewInteractions used in tests
    private val addFab = viewWithId(R.id.fab_add_task)
    private val taskTitleField = viewWithId(R.id.add_task_title)
    private val taskDescriptionField = viewWithId(R.id.add_task_description)
    private val editDoneFab = viewWithId(R.id.fab_edit_task_done)
    private val shareMenuItem =
            onView(allOf(withId(R.id.title), withText(R.string.share)))
    @Before
    fun setUp() {
        toDoTitle = TestData.getToDoTitle()
        toDoDescription = TestData.getToDoDescription()
    }
    @Before
    fun stubAllExternalIntents() {
        // By default Espresso Intents does not stub any Intents. Stubbing needs to be setup before
        // every test run. In this case all external Intents will be blocked.
        intending(not(isInternal()))
                .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
    }
    @Test
    fun stubsShareIntent() {
        // adding new TO-DO
        addFab.click()
        taskTitleField.type(toDoTitle).closeKeyboard()
        taskDescriptionField.type(toDoDescription).closeKeyboard()
        editDoneFab.click()
        // verifying new TO-DO with title is shown in the TO-DO list
        viewWithText(toDoTitle).checkDisplayed()
        openContextualActionModeOverflowMenu()
        shareMenuItem.click()
        //viewWithText(toDoTitle).click()
    }
}

Our class contains a simple test that adds a new TO-DO item and then clicks on the share button from the action bar menu. As you can see, we use the IntentsTestRule and stubAllExternalIntents() method .

The stubsShareIntent() test adds a new TO-DO item in the list, opens the action bar menu, and clicks on the Share option, which from its side, triggers the share intent to send it to the system. In a real use case, the system will redirect this intent to another application. If the system has more than one application that can handle the intent, a popup window showing the options will appear.

In our case, the stubsAllExternalIntents() method that is run before each test method will do its job and the intent will not go out of the application. Try to run the test and see the result. Figure 5-2 shows the end state of the application after the last test method step.
../images/469090_1_En_5_Chapter/469090_1_En_5_Fig2_HTML.jpg
Figure 5-2

The final state of the stubsShareIntent() test with stubbed external intents

Let’s see what happens when external intent stubbing is not in place⎯just comment out the stubAllExternalIntents() method and run the test again. Figure 5-3 shows the final application state.
../images/469090_1_En_5_Chapter/469090_1_En_5_Fig3_HTML.jpg
Figure 5-3

The final state of the stubsShareIntent() test without stubbed external intents

You see the difference and proof that intent stubbing works. The thing is that in both cases, the test passes. But in the second case, it passes just because we don’t have any additional steps after the intent is sent. If you comment out this line of code, which follows the moment the intent is stubbed:
//viewWithText(toDoTitle).click()

and run the test again, you will see that the test fails. Uncommenting the stubAllExternalIntents() method will make the test green again.

Stubbing a Single Intent

We just saw how all external application intents are stubbed, but what if we want to stub just one intent? Then the only thing we have to do is replace the intentMatcher from the following expression with a specific one using the intent matchers:
intending(intentMatcher).thenRespond(myResponse)

The share TO-DOs intent implementation looks the following way.

Share Intent Java Implementation from the com.example.android.architecture.blueprints.todoapp.tasks.TasksFragment.java Class.
String email = PreferenceManager
        .getDefaultSharedPreferences(getContext())
        .getString("email_text", "");
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT, getTaskListAsArray());
shareIntent.putExtra(Intent.EXTRA_EMAIL, email);
startActivity(Intent.createChooser(shareIntent,
getResources().getText(R.string.share_to)));
First, we will brake down the intent implementation and see what intent matchers can be applied to it:
  • shareIntent.setAction(Intent.ACTION_SEND)⎯This intent property can be matched by an hasAction() intent matcher.

  • shareIntent.setType("text/plain")⎯Can be matched by the hasType() intent matcher.

  • shareIntent.putExtra(Intent.EXTRA_TEXT, getTaskListAsArray()) and shareIntent.putExtra(Intent.EXTRA_EMAIL, email)⎯Can be matched by the hasExtra() or hasExtras() intent matchers.

It looks simple and clear, so to show how intent matchers can be implemented for each case, open the chapter5.StubIntentTest.kt class. Its implementation is similar to the chapter5.StubAllIntentsTest.kt class, but instead of applying external intents stubbing for each test method, we apply them on the method level, where specific intent matchers are applied.

chapter5.StubIntentTest.kt  Class Shows How to Stub Intents Using Different Intent Matchers.
class StubIntentTest {
    private var toDoTitle = ""
    private var toDoDescription = ""
    // ViewInteractions used in tests
    private val addFab = viewWithId(R.id.fab_add_task)
    private val taskTitleField = viewWithId(R.id.add_task_title)
    private val taskDescriptionField = viewWithId(R.id.add_task_description)
    private val editDoneFab = viewWithId(R.id.fab_edit_task_done)
    private val shareMenuItem =
            onView(allOf(withId(R.id.title), withText(R.string.share)))
    @get:Rule
    var intentsTestRule = IntentsTestRule(TasksActivity::class.java)
    @Before
    fun setUp() {
        toDoTitle = TestData.getToDoTitle()
        toDoDescription = TestData.getToDoDescription()
    }
    @Test
    fun stubsShareIntentByAction() {
        Intents.intending(hasAction(equalTo(Intent.ACTION_SEND)))
                .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
        // adding new TO-DO
        addFab.click()
        taskTitleField.type(toDoTitle).closeKeyboard()
        taskDescriptionField.type(toDoDescription).closeKeyboard()
        editDoneFab.click()
        // verifying new TO-DO with title is shown in the TO-DO list
        viewWithText(toDoTitle).checkDisplayed()
        //open menu and click on Share item
        openContextualActionModeOverflowMenu()
        shareMenuItem.click()
        viewWithText(toDoTitle).click()
    }
    @Test
    fun stubsShareIntentByType() {
        Intents.intending(hasType("text/plain"))
                .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
        // adding new TO-DO
        addFab.click()
        taskTitleField.type(toDoTitle).closeKeyboard()
        taskDescriptionField.type(toDoDescription).closeKeyboard()
        editDoneFab.click()
        // verifying new TO-DO with title is shown in the TO-DO list
        viewWithText(toDoTitle).checkDisplayed()
        //open menu and click on Share item
        openContextualActionModeOverflowMenu()
        shareMenuItem.click()
        viewWithText(toDoTitle).click()
    }
    @Test
    fun stubsShareIntentByExtra() {
        Intents.intending(hasType("text/plain"))
                .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
        // adding new TO-DO
        addFab.click()
        taskTitleField.type(toDoTitle).closeKeyboard()
        taskDescriptionField.type(toDoDescription).closeKeyboard()
        editDoneFab.click()
        // verifying new TO-DO with title is shown in the TO-DO list
        viewWithText(toDoTitle).checkDisplayed()
        //open menu and click on Share item
        openContextualActionModeOverflowMenu()
        shareMenuItem.click()
        viewWithText(toDoTitle).click()
    }
}

And after running all these tests, you might be surprised to see that they fail. After starting to analyze the intent implementation from com.example.android.architecture.blueprints.todoapp.tasks.TasksFragment.java, we can clearly see what action, type, and extra parameters are set to our intent. Why then do they fail?

After debugging and drilling down to the implementation of how our share intent is launched, as follows:
startActivity(Intent.createChooser(
       shareIntent,
       getResources().getText(R.string.share_to)));
We can see that the Android Intent.createChooser() method was used to send this intent to the system with a custom title. This method wraps the provided intent parameter with a specified action, type, and extra parameters into another intent with a new action and adds our intent as part of its extra parameters. Figure 5-4 shows how it looks when you try to debug what is happening.
../images/469090_1_En_5_Chapter/469090_1_En_5_Fig4_HTML.jpg
Figure 5-4

ShareIntent instance implemented in the com.example.android.architecture.blueprints.todoapp.tasks.TasksFragment.java class

The initial intent looks the same way we expect it to with the proper action (see the highlighted mAction variable) and proper extra parameters (see the highlighted mExtras variable). But if we put the debug breakpoint inside the IntentMatchers.hasExtras() matcher in the place where the intents are compared, we can see Figure 5-5.
../images/469090_1_En_5_Chapter/469090_1_En_5_Fig5_HTML.jpg
Figure 5-5

Hitting the breakpoint when tapping the Share menu item during test execution

At this moment in time, it is clear that the initial intent was added as an extra parameter inside the new intent (see the highlighted mExtras variable) with a modified action (see the highlighted mAction variable).

Now, to make our stubsShareIntentByAction() test green, we can change the action to ACTION_CHOOSER.

chapter5.StubChooserIntentTest.kt  Class .
    @Test
    fun stubsShareIntentByAction() {
        Intents.intending(hasAction(equalTo(Intent. ACTION_CHOOSER)))
                .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
        // adding new TO-DO
        addFab.click()
        taskTitleField.type(toDoTitle).closeKeyboard()
        taskDescriptionField.type(toDoDescription).closeKeyboard()
        editDoneFab.click()
        // verifying new TO-DO with title is shown in the TO-DO list
        viewWithText(toDoTitle).checkDisplayed()
        //open menu and click on Share item
        openContextualActionModeOverflowMenu()
        shareMenuItem.click()
        viewWithText(toDoTitle).click()
    }
Here are examples of working intent matchers when the Intent.createChooser() method is used to start the intent implemented in the chapter5.StubChooserIntentTest.kt class.
  • Based on initial intent action:

Intents.intending(hasAction(equalTo(Intent.ACTION_CHOOSER)))
                .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
  • Based on initial intent type:

Intents.intending(hasExtras(hasEntry(Intent.EXTRA_INTENT, hasType("text/plain"))))
        .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
  • Based on the EXTRA_TITLE parameter:

Intents.intending(hasExtras(hasEntry(Intent.EXTRA_TITLE, "Share to")))
        .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
And finally, to make the test from the chapter5.StubIntentTest.kt class pass, we change the way that the share TO-DO intent starts by replacing line 192 of the com.example.android.architecture.blueprints.todoapp.tasks.TasksFragment.java class:
startActivity(Intent.createChooser(shareIntent, getResources().getText(R.string.share_to)));
with this:
startActivity(shareIntent);
This way, the intent is not modified, and we fully rely on the system to show the popup to the user (see Figure 5-6).
../images/469090_1_En_5_Chapter/469090_1_En_5_Fig6_HTML.jpg
Figure 5-6

The share todo intent sent with Intent.createChooser() (left) and without using the Intent.createChooser() method (right)

Exercise 14

Stubbing intents
  1. 1.

    Put a breakpoint on line 191 of the TaskFragment.java file, as shown in Figure 5-4, and on line 204 of the IntentMatchers.java file, as shown in Figure 5-5. Run tests from the StubIntentTest.kt file in debug mode. When you reach the breakpoints, observe the shareIntent and intent variables.

     
  2. 2.

    Run all tests from the StubIntentTest.kt class and check the result. The test should fail. In the TaskFragment.java file, comment out line 191 and uncomment line 192. Run the test again and verify that they pass.

     
  3. 3.

    Revert to the changes done in Step 2 and run all the tests from the StubChooserIntentTest.kt class. The tests should all pass.

     

Stubbing Intents with the Result

In many cases, activity intents started by the application being tested have to return the results in the form of the image from a gallery or in form of a file from the device’s filesystem. In Android, this is achieved by starting an activity using the startActivityForResult() method from inside the application’s activity or fragment. When an activity is started, the user takes some action that generates the result and this result is returned to the activity or fragment that initially sent the intent. The onActivityResult() method from the Android activity class is responsible for receiving the result from a previous call to the startActivityForResult() method.

The sample TO-DO application contains an example of sending an intent with startActivityForResult() and handles the result in the onActivityResult() method implemented in the com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskFragment.java class.

Starting and Handling Image Intents in AddEditTaskFragment.java
public void onImageButtonClick() {
    Intent intent = new Intent();
    intent.setType("image/*");
    intent.setAction(Intent.ACTION_GET_CONTENT);
    startActivityForResult(intent, SELECT_PICTURE);
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (resultCode == RESULT_OK) {
        if (requestCode == SELECT_PICTURE) {
            Uri selectedImageUri = data.getData();
            BitmapDrawable bitmapDrawable =
                    ImageUtils.scaleAndSetImage(selectedImageUri, getContext(), 200);
            // Apply the scaled bitmap
            imageView.setImageDrawable(bitmapDrawable);
            // Now change ImageView's dimensions to match the scaled image
            ConstraintLayout.LayoutParams params =
                    (ConstraintLayout.LayoutParams) imageView.getLayoutParams();
            params.width = imageView.getWidth();
            params.height = imageView.getHeight();
            imageView.setLayoutParams(params);
        }
    }
}

You can observe that the intent from the onImageButtonClick() method has a preset type and action, which can be used in tests to match the intent and stub it.

The mechanism of starting an activity for a result should be clear now. The last thing we have to do is create the result used for stubbing. In the previous paragraph, we used the mechanism of returning the result, but we were setting it to null mainly because we were not expecting a result in the share intent case:
intending(not(isInternal()))
                .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
Now we need to implement the result on our own. We will discuss two ways of getting the result with stubbed images from an activity launched by the startActivityForResult() method :
  • Providing the result with the image file stored in the test application drawables.

  • Providing the result with the image file stored in the test application assets folder.

In Figure 5-7, you can observe the todo_image_drawable.png and todo_image_assets.png files stored in the test application res/drawable-xxxhdpi and assets folders, respectively.
../images/469090_1_En_5_Chapter/469090_1_En_5_Fig7_HTML.jpg
Figure 5-7

The location of the .png files used in intents stubbing

To showcase the implementation of both approaches, the sample application contains the chapter5.StubSelectImageIntentTest.kt class with test cases and the chapetr5.IntentHelper.kt object that holds methods responsible for generating the activity results used in intents stubbing.

Test Methods Implemented in the StubSelectImageIntentTest.kt Class
@Test
fun stubsImageIntentWithDrawable() {
    val toDoImage =
 com.example.android.architecture.blueprints.todoapp.mock.test.R.drawable.todo_image
    Intents.intending(not(isInternal()))
            .respondWith(IntentHelper.createImageResultFromDrawable(toDoImage))
    // Adding new TO-DO.
    addFab.click()
    taskTitleField.type(toDoTitle).closeKeyboard()
    taskDescriptionField.type(toDoDescription).closeKeyboard()
    // Click on Get image from gallery button. At this point stubbed image is returned.
    addImageButton.click()
    editDoneFab.click()
    viewWithText(toDoTitle).click()
}
@Test
fun stubsImageIntentWithAsset() {
    val imageFromAssets = "todo_image_assets.png"
    Intents.intending(not(isInternal()))
            .respondWith(IntentHelper.createImageResultFromAssets(imageFromAssets))
    // Adding new TO-DO.
    addFab.click()
    taskTitleField.type(toDoTitle).closeKeyboard()
    taskDescriptionField.type(toDoDescription).closeKeyboard()
    // Click on Get image from gallery button. At this point stubbed image is returned.
    addImageButton.click()
    editDoneFab.click()
    viewWithText(toDoTitle).click()
}
IntentHelper.kt Objects That Provides Methods Responsible for Generating the Activity Results Used in Intents Stubbing.
object IntentHelper {
    /**
     *  Creates new activity result from an image stored in test application drawable.
     *  See {@link Activity#setResult} for more information about the result.
     */
    fun createImageResultFromDrawable(drawable: Int): Instrumentation.ActivityResult {
        val resultIntent = Intent()
        val testResources = InstrumentationRegistry.getContext().resources
        // Build a stubbed result from drawable image.
        resultIntent.data = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
                + "://${testResources.getResourcePackageName(drawable)}"
                + "/${testResources.getResourceTypeName(drawable)}"
                + "/${testResources.getResourceEntryName(drawable)}")
        return Instrumentation.ActivityResult(Activity.RESULT_OK, resultIntent)
    }
    /**
     *  Creates new activity result from an image stored in test application assets.
     *  See {@link Activity#setResult} for more information about the result.
     */
    fun createImageResultFromAssets(imageName: String): Instrumentation.ActivityResult {
        val resultIntent = Intent()
        // Declare variables for test and application context.
        val testContext = InstrumentationRegistry.getContext()
        val appContext = InstrumentationRegistry.getTargetContext()
        val file = File("${appContext.cacheDir}/todo_image_temp.png")
        // Read file from test assets and save it into main application cache todo_image_temp.png.
        if (!file.exists()) {
            try {
                val inputStream = testContext.assets.open(imageName)
                val fileOutputStream = FileOutputStream(file)
                val size = inputStream.available()
                val buffer = ByteArray(size)
                inputStream.read(buffer)
                inputStream.close()
                fileOutputStream.write(buffer)
                fileOutputStream.close()
            } catch (e: Exception) {
                throw RuntimeException(e)
            }
        }
        // Build a stubbed result from temp file.
        resultIntent.data = Uri.fromFile(file)
        return Instrumentation.ActivityResult(Activity.RESULT_OK, resultIntent)
    }
}

The stubsImageIntentWithDrawable() test case stubs the intent result with an image located in the test application drawables and stubsImageIntentWithAsset() does the intent stubbing with an image stored in the test application assets folder.

Storing all the test images and files inside the test application is really convenient because the main application does not store any unnecessary test data. In this same way, we can store all the file types that may be used in intents stubbing.

Exercise 15

Stubbing Intents with the Result
  1. 1.

    Run all the tests from the current section and observe them passing. Replace the images in the res/drawable-xxxhdpi and assets folders with different ones. Run the tests again.

     
  2. 2.

    Based on the image intent implemented in AddEditTaskFragment.java, change the Intents.intending(not(isInternal())) implementation and replace the not(isInternal()) part with a hasAction() IntentMatcher.

     
  3. 3.

    Do the same change as in Step 2, but instead of hasAction(), use a hasType() IntentMatcher.

     

Verifying Intents

As of now, we are armed with the knowledge of intent matchers and have used them in test examples. It is time to move to the topic of verifying intents.

Along with the Intents.intending() mechanism for intent stubbing, Espresso provides Intents.indended() for intent validation. This mechanism records all intents that attempt to launch activities from the application being tested. Using the intended() method , you can assert that a given intent has been seen. A lot of information and examples about intents matching was provided in previous section, so we provide the same intent matchers to the intended() method.

Note

Even if we stub intents, they can be further validated using the intended() method.

To see intended() in action, let’s modify the existing stubsImageIntentWithDrawable() test as follows.

chapter5. StubSelectImageIntentTest.stubsImageIntentWithAsset() .
@Test
fun stubsImageIntentWithAsset() {
    val imageFromAssets = "todo_image_assets.png"
    Intents.intending(not(isInternal()))
            .respondWith(IntentHelper.createImageResultFromAssets(imageFromAssets))
    // Adding new TO-DO.
    addFab.click()
    taskTitleField.type(toDoTitle).closeKeyboard()
    taskDescriptionField.type(toDoDescription).closeKeyboard()
    // Click on Get image from gallery button. At this point stubbed image is returned.
    addImageButton.click()
    // Validate sent intent action.
    intended(hasAction(Intent.ACTION_GET_CONTENT))
    editDoneFab.click()
    viewWithText(toDoTitle).click()
}

In the current implementation, the tests pass because the image intent has set the ACTION_GET_CONTENT action. Of course, we can use the allOf() hamcrest matcher to combine different IntentMatchers and narrow down our validation.

Sometimes you may not see the intent implementation, but there is still a way to get all intents inside the Espresso failure stacktrace when the intended validation fails.

Part of Espresso Stacktrace from a Failed intended(intentMatcher) Validation.
IntentMatcher: has action: is "android.intent.action.ANSWER"
Matched intents:[]
Recorded intents:
-Intent { cmp=com.example.android.architecture.blueprints.todoapp.mock/com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskActivity } handling packages:[[com.example.android.architecture.blueprints.todoapp.mock]])
-Intent { act=android.intent.action.GET_CONTENT typ=image/* } handling packages:[[com.android.documentsui, com.google.android.apps.docs, com.google.android.apps.photos]])
This stacktrace was received after setting the wrong intent action in the previous test method to:
intended(hasAction(Intent.ACTION_ANSWER))
From the stacktrace, we can see that among the image intents:
 { act=android.intent.action.GET_CONTENT typ=image/* }
There is another one:
{ cmp=com.example.android.architecture.blueprints.todoapp.mock/com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskActivity }

To understand how intents appear in the stacktrace, let’s take a closer look at the Espresso Intents.java class. This class is responsible for validating and stubbing intents sent out by the application being tested. It contains the init() method , which initializes intents and begins recording them. It must be called prior to triggering any actions that send out intents that need to be verified or stubbed. And it is because it is used by the IntentsTestRule that it’s required to run intent tests.

Having this information, we can add modifications to the stubsImageIntentWithAsset() test case. We will also verify that after clicking the Add TO-DO floating action button, the AddEditTaskActivity is launched.

Modified stubsImageIntentWithAsset() Test Case.
@Test
fun stubsImageIntentWithAsset() {
    val imageFromAssets = "todo_image_assets.png"
    Intents.intending(not(isInternal()))
            .respondWith(IntentHelper.createImageResultFromAssets(imageFromAssets))
    // Adding new TO-DO.
    addFab.click()
    // Validate that AddEditTaskActivity was launched.
    intended(hasComponent(AddEditTaskActivity::class.java.name))
    taskTitleField.type(toDoTitle).closeKeyboard()
    taskDescriptionField.type(toDoDescription).closeKeyboard()
    // Click on Get image from gallery button. At this point stubbed image is returned.
    addImageButton.click()
    // Validate sent intent action.
    intended(hasAction(Intent.ACTION_GET_CONTENT))
    editDoneFab.click()
    viewWithText(toDoTitle).click()
}
It is also important to pay attention to the stacktrace intent details and debug information, as shown in Figures 5-4 and 5-5. Both of these sources contain information about intents, like its action, type, or component. Let’s take one more look at the stacktrace:
Recorded intents:
-Intent { cmp=com.example.android.architecture.blueprints.todoapp.mock/com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskActivity } handling packages:[[com.example.android.architecture.blueprints.todoapp.mock]])
-Intent { act=android.intent.action.GET_CONTENT typ=image/* } handling packages:[[com.android.documentsui, com.google.android.apps.docs, com.google.android.apps.photos]])
As you may guess:
  • cmp⎯Stands for component. Applies hasComponent() IntentMatcher.

  • packages⎯Stands for package. Applies hasPackage() or toPackage() IntentMatcher.

  • act⎯Stands for action. Applies hasAction() IntentMatcher.

  • typ⎯Stands for type. Applies hasType() IntentMatcher.

Exercise 16

Verifying Intents
  1. 1.

    Modify one of the intent tests and make it fail at the moment of intent validation with the intended() method. Observe the stacktrace.

     
  2. 2.

    Implement a test that verifies the share intent functionality discussed in the “Stubbing Intents Without Result” section. Make the verification based on the intent type and action. Use the allOf() hamcrest matcher to validate both of them.

     

Summary

Espresso-Intents enables you to keep your UI tests hermetic, without the need to interact with third-party applications, and allows you to validate intents sent within or outside of the application being tested. It is a powerful mechanism that helps you test and stub application intents. After you get familiar with it, it will improve your overall Android system knowledge since the majority of communication among application components, applications, and the system is done through intents.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset