/ SOFTWARE-TESTING, ANDROID

Android Testing with Espresso — part 5

This post was translated from Portuguese using generative AI.

In the previous post, we learned how to mock Android’s intents. If you want to start from this post, use the branch ‘part_4’ of the project.

Assertions on the RecyclerView item’s layout

In our MainActivity, the next step is to verify whether the layout we defined for the item of our RecyclerView is displayed correctly. Let’s take a look at our item’s layout.

User Item Image

It’s a rather simple layout:

  • An ImageView with id user_view_image;
  • A TextView with id user_view_name.

Then, let’s write a test that checks if these views are on the screen:

@Test
public void checkUserItemView_isDisplayed() {
  server.enqueue(new MockResponse().setResponseCode(200).setBody(Mocks.SUCCESS));
  mActivityRule.launchActivity(new Intent());
  onView(withId(R.id.user_view_image)).check(matches(isDisplayed()));
  onView(withId(R.id.user_view_name)).check(matches(isDisplayed()));
}

We queue a successful request, thus, our recycler is displayed. Then, we make a simple assertion on the ids that we have in the list item. Run this test, the result should be this:

AmbiguousViewMatcherException: 'with id: com.example.heitorcolangelo.espressotests:id/user\_view\_image' **matches multiple views in the hierarchy.**

The test did not work, and Espresso is telling us that there are several views with the id we passed, after all, there are several identical items in the list, ie several identical ids.

The way I usually solve this problem may seem a bit lazy, but it has worked very well so far. Just mock it with only one item in the list! :)

@Test
public void checkUserItemView_isDisplayed() {
  server.enqueue(new MockResponse().setResponseCode(200).setBody(Mocks.SUCCESS_SINGLE_ITEM));
  mActivityRule.launchActivity(new Intent());
  onView(withId(R.id.user_view_image)).check(matches(isDisplayed()));
  onView(withId(R.id.user_view_name)).check(matches(isDisplayed()));
}

The test will be like this, we only change the body that we pass in the MockResponse, to pass the mock with only one item. Run the test again, it should pass. As we only have one item from the list displayed on the screen, we no longer have the problem of several identical ids. Now we can make the assertion easily.

Of course, I will not be simplistic, this will not always be enough. For example, if you have different types of layout on your list. So, let’s do the same test without using the above “trick”.

@Test
public void checkUserItemView_isDisplayed_noTricks() {
  server.enqueue(new MockResponse().setResponseCode(200).setBody(Mocks.SUCCESS));
  mActivityRule.launchActivity(new Intent());
  onView(allOf(withId(R.id.user_view_image), hasSibling(withText("Eddie Dunn")))).check(matches(isDisplayed()));
  onView(allOf(withId(R.id.user_view_name), withText("Eddie Dunn"))).check(matches(isDisplayed()));
}

We use two new methods:

  • allOf: matches all the conditions that we pass as a parameter. In this case, we pass withId and hasSibling,
  • hasSibling: sibling in English means “brother”. This method searches for the view that has a “brother” that meets the conditions we pass as a parameter. In this case, we pass withText.

In line 5 we tell Espresso: “Check if the view that has the id user_view_image and the brother with text “Eddie Dunn” is visible on the screen.” Thus, we avoid an AmbiguousViewMatcherException, as there is only one view that meets the above conditions.

At this point, you might wonder if it’s correct to directly use the text “Eddie Dunn” for the assertion. After all, what if the item named “Eddie Dunn” is not visible on the screen at first? I can do this without problems, as I know that the first item in the list will always be “Eddie Dunn”, since we are using a mocked response.

RecyclerViewActions

So far, we have only made assertions in the items of our list. Now we will start making interactions, using the methods of the RecyclerViewActions class. For this, add this dependency in your build.gradle file:

androidTestCompile("com.android.support.test.espresso:espresso-contrib:$espressoVersion") {  
  exclude module: 'appcompat-v7'  
  exclude module: 'support-v4'  
  exclude module: 'support-annotations'  
  exclude module: 'recyclerview-v7'  
  exclude module: 'design'  
}

Now, let’s test the following scenario:

  • When clicking on an item from the list, UserDetailsActivity should open.
@Test
public void whenClickOnItemList_shouldStartUserDetailsActivity_withExtra() {
  server.enqueue(new MockResponse().setResponseCode(200).setBody(Mocks.SUCCESS));
  mActivityRule.launchActivity(new Intent());
  Intents.init();
  Matcher<Intent> matcher = allOf(
    hasComponent(UserDetailsActivity.class.getName()),
    hasExtraWithKey(UserDetailsActivity.CLICKED_USER)
  );
  
  Instrumentation.ActivityResult
    result = new Instrumentation.ActivityResult(Activity.RESULT_OK, null);
  
  intending(matcher).respondWith(result);
  
  onView(withId(R.id.recycler_view)).perform(actionOnItemAtPosition(0, click()));
  
  intended(matcher);
  Intents.release();
}

Let’s go through this test line-by-line:

  • Lines 3, 4 and 5: Nothing new, just queueing the requests that will be made, starting the activity and the intent service.
  • Line 6: We create an intent matcher the same as we did in the test of the LoginActivityTest. The only difference is that we add another matcher as condition, using the hasExtraWithKey method. That is, besides checking if the launched intent is for the UserDetailsActivity, we verify if it contains an extra, which is the clicked user. Therefore, we pass the UserDetailsActivity.CLICKED_USER as parameter. This value is the same that is assigned to the key of our intent bundle, when we want to launch UserDetailsActivity. For better understanding, check the code in the MainActivity.
  • Lines 11 to 14: No innovation, just setting what we will pass as result for the intent that will be launched.
  • Line 16: We use the actionOnItemAtPosition(int position, ViewAction viewAction)_ method,_ of the RecyclerViewActions class, which contains various other methods to interact with our list. There’s no secret, it performs the action at the position we pass as parameter.

Run all the tests, they should pass. If something went wrong, go over the previous steps or leave a comment so I can help. At the end of this step, your code should look similar to the one in the ‘part_5’ branch.

Let’s move on to testing our last activity, the UserDetailsActivity.

Go to Part 6 — Custom Matchers and Runtime Permissions»