Let's say you want to write a user interface test that matches the child view at a particular position within a
RecyclerView
and you want to assert some properties on that child view. When you search for this online, you'll come across a whole host of solutions that do work but, sadly, are not very fluent or Espresso-esque. You'll see solutions that will lead you to write assertions like the following:
onView(withRecyclerView(R.id.recyclerView).atPositionOnView(0))
.check(matches(withText("Some Text")))
onView(withId(R.id.recyclerView))
.check(matches(atPosition(0, withText("Some Text"))))
Like I said: sure, this works, but I feel we can do better. Most notably, what I would change about these is to match on the child view of interest instead and assert properties on that rather than matching on the entire
RecyclerView
. So what we're aiming for are assertions like the following:
onView(withPositionInRecyclerView(R.id.recyclerView, 0))
.check(matches(withText("Some Text")))
It's a subtle change and maybe it's just me but I feel this is a little easier on the eye and makes the reading experience a little more pleasurable. If you've made it this far and you're bought in, here's the
Matcher
class you need to define to make this possible:
/**
* A matcher that matches the child [View] at the given position
* within the [RecyclerView] which has the given resource id.
*
* Note that it's necessary to scroll the [RecyclerView] to the desired position
* before attempting to match the child [View] at that position.
*/
class WithPositionInRecyclerViewMatcher(private val recyclerViewId: Int,
private val position: Int) : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("with position $position in RecyclerView which has id $recyclerViewId")
}
override fun matchesSafely(item: View): Boolean {
val parent = item.parent as? RecyclerView
?: return false
if (parent.id != recyclerViewId)
return false
val viewHolder: RecyclerView.ViewHolder = parent.findViewHolderForAdapterPosition(position)
?: return false // has no item on such position
return item == viewHolder.itemView
}
}
And, for best practice, define a function that wraps this class as follows:
/**
* @return an instance of [WithPositionInRecyclerViewMatcher] created with the given parameters.
*/
fun withPositionInRecyclerView(recyclerViewId: Int, position: Int): Matcher<View> {
return WithPositionInRecyclerViewMatcher(recyclerViewId, position)
}
That's it. We're done. Happy testing and for more view matchers like this one, check out the android-test-utils repository.