Tuesday, June 7, 2016

Espresso and the RecyclerView

Recently I had a situation where I needed an integration level test to verify a particular action on a CardView caused the UI to update in all of the expected places. The CardView is was element served by the adapter in a RecyclerView, and on the CardView was a favorite action that would toggle based on the user's desire.

I decided to use Espresso for this test. Up to now, I sort of avoided UI tests, because they were always reasonably complex to create and for the most part I was able to test business logic with plain unit tests. I knew a lot of effort had been put into Android on the testing front and had watched a good presentation from Google I/O 2016 on Espresso, so I decided to give it a try.

The test plan

  1. Scroll the recycler view to the appropriate position
  2. Check to see that the favorite status on the card was currently "not favorited"
  3. Click the "favorite" button to toggle the favorite status
  4. Check to see that the favorite status was updated to "favorited"


Getting started with Espresso for RecyclerView


Seemed simple enough, so I started to look at the Espresso API for interacting with the RecyclerView. After discovering the RecyclerViewActions, I decided to create a simple test for "clicking" a specific position in the RecyclerView to get started.

As you can see there are two steps. First is positioning the RecyclerView and then next is interacting with it. Positioning is optional if the particular row is on screen. However, I discovered that if the test needs to actually perform a scroll, you need to do that as a separate action or the click would fail more times than not.

The catch


All great so far. I essentially accomplished step 1 in my test case, but interestingly most examples only go that far. Now I needed to do steps 2-4 on the favorite button inside the card view found by step 1. I tried different APIs with Espresso, but nothing would let me interact with the favorite button. After searching the web and StackOverflow, I found that most people solved it by creating a one off action that clicked the interacted with the descendant view. This essentially solved step 3 in my test, but not steps 2 and 4 which check the state of the view before and after "clicking" the button.

Creating the solution


What I wanted was a completely generic solution that both allowed a test to check the status of a view and perform actions on the view that would be a descendant of the view served by the RecyclerView adapter. I wanted to leverage the existing Espresso API and not rely on one off actions to do my bidding. Pretty quickly I found I could wrap any ViewAssertion within an action and then just invoke it in the call to perform. This gave me hope I could create a completely generic solution.

With a little more effort I was able to figure out how to create an action that took a view matcher and an action. The view matcher would search for the descendant view starting at the specific view at position X in the RecyclerView. After finding the view it would perform the action specified. Now that I had my re-usable actions, my test case look like:

I realized others would benefit from these actions to I created a library and published it to GitHub. While the library solves problems with RecyclerView, it has no direct dependencies on RecyclerView so it could solve other situations where you need to interact with a descendant view.

You may find the project here: EspressoDescendantActions

Let me know how it works for you and if you find any bugs.