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).
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:
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.
Or in case of AndroidX Test library usage, we need the following.
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.
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 .
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 .
Note
The method annotated with the @Before annotation will be executed for every test case before it’s run.
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.
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.
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.
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.
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.
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.
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
The share TO-DOs intent implementation looks the following way.
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.
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?
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.
Based on initial intent action:
Based on initial intent type:
Based on the EXTRA_TITLE parameter:
Exercise 14
- 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.
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.
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.
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.
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.
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.
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
- 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.
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.
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.
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.
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.
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
- 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.
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.