automation hacks

Share this post
Hello, espresso! Part 3 Working with intents šŸ”„
newsletter.automationhacks.io

Hello, espresso! Part 3 Working with intents šŸ”„

Learn how to automate android intents (with and without stubbing) using espresso

Gaurav Singh
May 11
Share this post
Hello, espresso! Part 3 Working with intents šŸ”„
newsletter.automationhacks.io

Things are getting pretty exciting 😁 as we work our way through learning espresso's API

In the last part Hello, espresso! Part 2 Working with lists, we learned how to work with list controls (RecyclerView,Ā AdapterView) in espresso. Go ahead and have a read in case you missed it.

Learning to test Intents šŸ’Ŗ

In this post, we'll understand how to automate testing of intents using espresso.

espresso-intentsĀ provides us the capabilities to validate intents for a couple of important use cases:

  • Test whether the correct intent is invoked with valid data by our app

  • Or, even stub out the intents sent out so that we can verify only our apps logic (while assuming other apps that we depend upon have been tested independently)

What is an Android intent?

InĀ developer.android.com's post about Android Intents

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

It can be used:

  • withĀ startActivityĀ to launch an Activity,

  • broadcastIntentĀ to send it to any interestedĀ BroadcastReceiverĀ components,

  • andĀ Context.startService(Intent)Ā orĀ Context.bindService(Intent, ServiceConnection, int)Ā to communicate with a background Service.

One of the use cases for whichĀ IntentsĀ are used a lot is toĀ launch ActivitiesĀ and they can be thought is a data structure that holds an abstract description of an action to be performed

There are 2 components to an Intent:

  • Action:Ā What action has to be performed, some examples of this are:

    • ACTION_VIEWĀ displays data to the user, for instance, if we use it asĀ tel: URIĀ it will invoke the dialer (we'll see this in our test example),

    • ACTION_EDITĀ gives explicit edit access to given data

  • Data:Ā Data to operate on expressed as aĀ Uri

You can read the full docĀ hereĀ to understand more about Intents

Understanding our App under test šŸ•µšŸ»

Let's start by understanding the app under test

We'll useĀ IntentsBasicSampleĀ app, which has anĀ EditTextĀ to enter a phone no and aĀ ButtonĀ to either call a number or randomly pick a no, if the user taps on the call number button it launches a dialer app

The below scenarios are possible

GIVEN user is on home screen
WHEN user taps on enter phone no EditText with id: @id/edit_text_caller_number
AND user types a valid phone no
AND user taps on call number Button with id: id	@id/button_call_number
THEN the dialer activity is shown with entered phone no

I've written this in Gherkin syntax for clarity, however in a live test, the tests should always describe behavior and no be as procedural as i've written below. ReadĀ thisĀ page on cucumber website to understand more

Let's use Layout inspector to grab the selectors for the elements we want to work with:

Add required dependencies

We need to addĀ espresso-intentsĀ dependency to ourĀ app/build.gradleĀ file as below, also it’s only compatible with espressoĀ 2.1+Ā and android testing lib versionĀ 0.3+, so we need to double-check their versions in our dependencies as well

androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
androidTestImplementation 'androidx.test:runner :1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

Test to launch a dialer activity using intents and validation

Below is the complete test to perform our scenario, don't worry šŸ§˜šŸ»ā€ā™‚ļø if it does not make sense right now, we'll unpack this in detail below, the complete example is mentioned so that you can skim through its structure first before we dive deeper

@RunWith(AndroidJUnit4.class)
@LargeTest
public class DialerActivityPracticeTest {
  public static final String VALID_PHONE_NUMBER = "123-456-7898";

  @Rule
  public GrantPermissionRule grantPermissionRule =
      GrantPermissionRule.grant("android.permission.CALL_PHONE");

  @Rule
  public ActivityScenarioRule<DialerActivity> testRule =
      new ActivityScenarioRule<>(DialerActivity.class);

  @Before
  public void setupIntents() {
    // Initializes intents and begins recording intents, must be called before
    // triggering any actions that call an intent that we want to verify with validation or stubbing
    init();
  }

  @After
  public void teardownIntents() {
    // Clears intent state, must be called after each test case
    release();
  }

  /** Test to enter a phone number and make a call and verify an intent is launched */
  @Test
  public void whenTapOnCallNumber_ThenOutgoingCallIsMade() {
    // Type a phone no and close keyboard
    Uri phoneNumber = Uri.parse("tel:" + VALID_PHONE_NUMBER);
    onView(withId(R.id.edit_text_caller_number)).perform(typeText(VALID_PHONE_NUMBER), closeSoftKeyboard());

    // Tap on call number button
    onView(withId(R.id.button_call_number)).perform(click());

    // Verify an intent is called with action and phone no and package
    intended(allOf(hasAction(Intent.ACTION_CALL), hasData(phoneNumber)));
  }
}

Setting up intents and permissions

  • Just likeĀ Views, we'll use JUnit rules to set up and tear down our intents before and after each test. We'll useĀ ActivityScenarioRuleĀ for this

  • Since we want to automate theĀ DialerActivityĀ class, we'll pass that as the generic type withinĀ <>

@Rule
  public ActivityScenarioRule<DialerActivity> testRule =
      new ActivityScenarioRule<>(DialerActivity.class);
  • ā—ļøHowever just adding the rule is not enough and we need to set up and tear down our intents before and after a test as well

    • We useĀ @BeforeĀ andĀ @AfterĀ JUnit annotations for this purpose and callĀ init()Ā before our test starts to execute andĀ release()Ā after the test has been executed

Note: If you read other blogs and even theĀ official google guide on espresso-intents, they show usage ofĀ IntentsTestRuleĀ for setting up intents, but this has recently been deprecated with suggestion to useĀ ActivityScenarioRuleĀ withĀ init()Ā andĀ release(). I'm sure these examples and docs would get updated soon, you can meanwhile refer to this blog šŸ˜‰. You can seeĀ this commitĀ to understand how your test code would look like prior and after this change.

@Before
  public void setupIntents() {
    // Initializes intents and begins recording intents, must be called before
    // triggering any actions that call an intent that we want to verify with validation or stubbing
    init();
  }

  @After
  public void teardownIntents() {
    // Clears intent state, must be called after each test case
    release();
  }
  • We also want our test to have permission to make a call and thus add the below snippet as another JUnit rule to ensure we don't get any permission errors during our test run

@Rule
public GrantPermissionRule grantPermissionRule = GrantPermissionRule.grant("android.permission.CALL_PHONE");

Writing core test logic

With that taken care of let's write our test

We store a test number in a static variable

public static final String VALID_PHONE_NUMBER = "123-456-7898";

We'll type the phone no into our EditText as below and then close the keyboard:

onView(withId(R.id.edit_text_caller_number)).perform(typeText(VALID_PHONE_NUMBER), closeSoftKeyboard());

We'll then tap the call number button

// Tap on call number button
onView(withId(R.id.button_call_number)).perform(click());

Asserting our intent was fired

Great šŸ™ŒšŸ¼, we want to verify that our Intent was actually invoked and we can achieve that by usingĀ intendedĀ method that takes an Intent matcher (either an existing one or one that we define).

Tip: šŸ’” You can refer toĀ Hamcrest TutorialĀ understand how hamcrest matchers work since we are going to use them heavily with espresso

// Verify an intent is called with action and phone no and package
intended(allOf(hasAction(Intent.ACTION_CALL), hasData(phoneNumber)));

If you notice, we useĀ allOf()Ā matcher, that checks that the examined object matchesĀ all of the specified matchers

We first check that theĀ intentĀ had the correct action by callingĀ hasAction(Intent.ACTION_CALL)

How do we know which action to assert? šŸ¤”

We can look into app source inĀ DialerActivityĀ to understand more details about our intent

If you look atĀ createCallIntentFromNumberĀ method, you can see we create an intent with actionĀ Intent.ACTION_CALL:

final Intent intentToCall = new Intent(Intent.ACTION_CALL);

Also, we see that we set a phone no as the intents data in:

intentToCall.setData(Uri.parse("tel:" + number));

Here is the full method for reference

private Intent createCallIntentFromNumber() {
  final Intent intentToCall = new Intent(Intent.ACTION_CALL);
  String number = mCallerNumber.getText().toString();
  intentToCall.setData(Uri.parse("tel:" + number));
  return intentToCall;
}

We also assert that our intent has the correct phone no set as data by:

Preparing the phone noĀ UriĀ earlier

Uri phoneNumber = Uri.parse("tel:" + VALID_PHONE_NUMBER);

and then add the below line in ourĀ allOfĀ matcher

hasData(phoneNumber)

Stubbing intent response

If you run this test, you'll see the Dialer Activity pop up

In the above test, we saw how espresso intents could launch another activity and we can quickly validate them usingĀ intended,

However, If we only care about testing the logic of our app and not so much about a 3rd party apps logic (since we anywaysĀ cannot manipulate the UIĀ of external activity, nor control theĀ ActivityResultĀ returned to the activity we are testing), then espresso allows us to stub intents and returns a mock response as well usingĀ intending

Let's see how we can do this:

We add the below line in ourĀ @BeforeĀ annotated setup method:

intending(not(isInternal()))
  .respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));

Let's understand its nuts and bolts:

  • We can configure espresso to return aĀ RESULT_OKĀ for any intent call by usingĀ isInternal()Ā intent matcher that checksĀ if an intents package is the same as the target package for the instrumentation test.

    • Since in this case, we want to stub out all intent calls to other activities we can wrap this with aĀ not()Ā so ensure we inverse the result of the matcher

  • We then ask espresso to return aĀ RESULT_OKĀ as a stubbed response by usingĀ respondWith()Ā and mention the result we want to return:

.respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));

Here:

  • Activity.RESULT_OKĀ is theĀ resultCodeĀ and

  • nullĀ is the resultData since we don't want to return anything in the intent response

If we rerun the above test, you'll see that no dialer activity is started since the intent call to external activities is going to be stubbed

Test our own apps intent without making an external activity call a.k.a Stubbing response

Let's see another example of stubbing usingĀ intendingĀ updated functional test flow

GIVEN user is on home screen
WHEN user taps on pick number Button with id: @id/button_pick_contact
THEN we check that an intent call was made
AND we verify intent response from Contacts Activity is stubbed

We can write the below test to achieve this flow:

@RunWith(AndroidJUnit4.class)
@LargeTest
public class DialerActivityPracticeTest {
  public static final String VALID_PHONE_NUMBER = "123-456-7898";

  @Rule
  public GrantPermissionRule grantPermissionRule =
      GrantPermissionRule.grant("android.permission.CALL_PHONE");

  @Rule
  public ActivityScenarioRule<DialerActivity> testRule =
      new ActivityScenarioRule<>(DialerActivity.class);

  @Before
  public void setupIntents() {
    // Initializes intents and begins recording intents, must be called before
    // triggering any actions that call an intent that we want to verify with validation or stubbing
    init();
  }

  @After
  public void teardownIntents() {
    // Clears intent state, must be called after each test case
    release();
  }

  @Test
  public void whenPickNumber_AndTapOnCallWithStub_ThenStubbedResponseIsReturned() {
    // To stub all intents to contacts activity to return a valid phone number
    // we use intending() and verify if component has class name ContactsActivity
    // then we responding with valid result
    intending(hasComponent(hasShortClassName(".ContactsActivity")))
        .respondWith(
            new Instrumentation.ActivityResult(
                Activity.RESULT_OK, ContactsActivity.createResultData(VALID_PHONE_NUMBER)));

    onView(withId(R.id.button_pick_contact)).perform(click());
    onView(withId(R.id.edit_text_caller_number)).check(matches(withText(VALID_PHONE_NUMBER)));
  }
}

In this example, we show that we could also selectively stub out intent calls to a particular activity, e.g. if we wanted all calls toĀ ContactsActivityĀ to return a code:Ā RESULT_OKĀ and a valid phone no, we can do so by writing:

intending(hasComponent(hasShortClassName(".ContactsActivity")))
        .respondWith(
            new Instrumentation.ActivityResult(
                Activity.RESULT_OK, ContactsActivity.createResultData(VALID_PHONE_NUMBER)));

Note: If we want to stub calls to all classes in a package we could use:Ā toPackageĀ method insideĀ intending

intending(toPackage("com.android.contacts")).respondWith(result);

Here we useĀ hasComponent(hasShortClassName(".ContactsActivity"))Ā that matches any call to classĀ ContactsActivityĀ and respond withĀ RESULT_OK, also we returnĀ resultDataĀ as the return value ofĀ createResultDataĀ method

If we see impl ofĀ createResultDataĀ inĀ ContactsActivityĀ source code, we see it returns an empty intent with a phone no value

@VisibleForTesting
    static Intent createResultData(String phoneNumber) {
        final Intent resultData = new Intent();
        resultData.putExtra(KEY_PHONE_NUMBER, phoneNumber);
        return resultData;
    }

We finally tap on the pick number button and verify that theĀ EditTextĀ button has the same no as the one returned by the stubbed intent call

And that's how you automate intents with espresso! āœ…

Resources

  • You can find the app and test code for this post on Github:

    • App

    • Test code

  • Please readĀ espresso-intentsĀ that talks about how to work with intents on Android developers

  • Refer to original source code onĀ testing-samples

    • IntentsBasicSample

    • IntentsAdvancedSample

Conclusion

Hopefully, this post gives you an idea of how to work with intents in espresso. Stay tuned for the next post where we’ll dive into how to automate and work withĀ idling resourcesĀ with espresso

As always, Do share this with your friends or colleagues and if you have thoughts or feedback, I’d be more than happy to chat over on Twitter or comments. Until next time. Happy Testing and learning.

Share this post
Hello, espresso! Part 3 Working with intents šŸ”„
newsletter.automationhacks.io
Comments

Create your profile

0 subscriptions will be displayed on your profile (edit)

Skip for now

Only paid subscribers can comment on this post

Already a paid subscriber? Sign in

Check your email

For your security, we need to re-authenticate you.

Click the link we sent to , or click here to sign in.

TopNew

No posts

Ready for more?

Ā© 2022 Gaurav Singh
Privacy āˆ™ Terms āˆ™ Collection notice
Publish on Substack Get the app
SubstackĀ is the home for great writing