AdapterViews and Espresso

Wojtek Kaliciński
Android Developers
Published in
3 min readJun 1, 2016

If there’s only one thing you read in this article before you get distracted and close this tab, it has to be this: use onData() instead of onView() when working with AdapterViews in your Espresso tests.

AdapterViews such as list, grids and spinners are different than the usual layouts (e.g. LinearLayout) because they don’t keep all their child elements in the view hierarchy. The main purpose of AdapterViews is to show large data sets on the screen efficiently, so they have to optimize memory use and performance by only maintaining a View object for data elements that currently fit inside the viewport.

All other elements exist only as the data set in the Adapter that is backing the AdapterView. With Espresso.onData(Matcher dataMatcher) you supply a Matcher that will try to match a row in the Adapter. If there is a successful match, Espresso will then bring that row onto the screen and into the view hierarchy so that you can perform actions and check assertions on its view as usual:

onData(Matcher dataMatcher)
.perform(ViewAction action)
.check(ViewAssertion assert)

The main difference from onView() is that you’ll be passing a Matcher that works with data supplied by the Adapter instead of a Matcher which looks for View attributes.

By default, onData() will search all AdapterViews that it finds on the current screen. If you want to limit that, you can use a ViewMatcher that will be used for locating the AdapterView you’re interested in:

onData(Matcher dataMatcher)
.inAdapterView(Matcher<View> adapterMatcher)
.perform(...)

Matching data from the Adapter

So how does the data matcher that you supply to onData() work? Basically, Espresso goes through the items in the Adapter backing your AdapterView one by one, and passes the result of Adapter.getItem(int position) to the Matcher.

Depending on what kind of data your Adapter holds as its items (and thus returns from Adapter.getItem), you will need to use different Matchers. Let’s say you have an ArrayAdapter holding an array of String objects, you could use basic text matchers:

onData(is(instanceOf(String.class)), is("Americano")));//partial text match:
onData(is(instanceOf(String.class)), containsString("ricano")));
//starts with text:
onData(is(instanceOf(String.class)), startsWith("Amer")));

If you’re using a SimpleAdapter that returns key-value pairs stored as a Map, there’s a matcher available to find the item that contains a given key-value entry:

onData(
is(instanceOf(Map.class)),
hasEntry(equalTo("jobTitle"), is("Barista")))
);

For CursorAdapters, we have built a set of matchers in Espresso that let you examine the Cursor values:

onData(
is(instanceOf(Cursor.class)),
CursorMatchers.withRowString("job_title", is("Barista"))
);

And finally, if you’re using your own data type for Adapter items (let’s assume it returns objects of type Person), you can write your own custom matcher to examine the properties you want:

public static Matcher withName(Matcher nameMatcher){
return new TypeSafeMatcher<Person>(){
@Override
public boolean matchesSafely(Person person) {
return nameMatcher.matches(person.getName());
}

@Override
public void describeTo(Description description) {
...
}
}
}

The case of the RecyclerView

At first it might seem like the same method should be applied to a RecyclerView, as it also uses adapters to hold data and reuses a small amount of View objects to display that data on the screen. Unfortunately, RecyclerView does not inherit from AdapterView (it’s a direct subclass of ViewGroup instead), so you can’t use onData with it.

Instead, you should use one of the RecyclerViewActions methods to scroll the RecyclerView to the desired item and perform a ViewAction on it (using a ViewHolder matcher or position):

onView(withId(R.id.myRecyclerView))
.perform(
RecyclerViewActions.actionOnItemAtPosition(0, click())
);

--

--