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.

Monday, September 7, 2015

The Unexpected Permission

Recently I've noticed an interesting trend where apps are suddenly asking for permission to write to external storage. Considering starting with Android 4.4, an application is no longer required to have this permission to read the application-specific directories, you would expect more apps to remove the permission instead of add it. With Android 6.0 on the horizon where each app was going to have to "justify" why they need each restricted permission and then be able to justify it to the user.

The Potential Cause

While the new influx of the permission was strange, it wasn't until I was recently testing the new Android 6.0 permission api that I noticed something odd. When I went to the app settings to "switch" off a permission that was granted, I noticed the app I was working on also had registered for the external storage permissions. This was especially troubling since the application had only declared the course location permission, beyond a few normal permissions such as internet in the manifest. 

I double checked the manifest file to ensure someone didn't leave the external storage permission configured by accident.  However, as expected, only the permissions I remembered were listed in the manifest. Needless to say I was momentarily confused. 

I then remembered a post by Ian Lake I had read a while back talking about how the Google Play Services libraries were automatically adding certain content to the manifests such as registering the Google Play Services version. Perhaps a library was adding the permission on the apps behalf. Sure enough, the final merged manifest for the application had these lines merged into the application's manifest:

    android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    android:name="android.permission.READ_EXTERNAL_STORAGE" />
    android:glEsVersion="0x00020000" android:required="true" />

I immediately recognized the write external storage permission and the OpenGL v2 requirement as part of the Google Maps V2 API. However, my app was not using the Maps api, so I ran the following Gradle command to see which library was potentially including Maps:

./gradlew androidDependencies

which showed this snippit:

\--- com.google.android.gms:play-services-location:7.8.0
     +--- com.google.android.gms:play-services-base:7.8.0
     |    \--- com.android.support:support-v4:23.0.0
     |         \--- LOCAL: internal_impl-23.0.0.jar
     \--- com.google.android.gms:play-services-maps:7.8.0
          \--- com.google.android.gms:play-services-base:7.8.0
               \--- com.android.support:support-v4:23.0.0
                    \--- LOCAL: internal_impl-23.0.0.jar

As you can see Google Play Location now depends on Maps V2. After a little research I determined that the smaller Google Play Service libraries are utilizing manifest merging to automatically add permissions and other attributes developers used to have to add manually. By updating to a newer version of the Location API, all applications that just use the location API will require new permissions added by its dependency on the maps API. Since most of the apps I had notice adding the external storage permission did use location in some form, I wonder if this is where the influx of external storage permissions is coming from.

Since the application I was testing wasn’t using Maps V2, what should be done about the extra permission? For Android 6.0 marshmallow, the permission would never be activated since the app wouldn’t directly request it. However, for Lollipop and below, the permission would have to be accepted by the user to just to install or update the app. This would potentially hold back automatic updates because they user would have to accept the new permissions in order to get the latest version. This is a tough ask when the new permissions are not even required for the app to operate correctly. 

The Solution

After a little research, I discovered it is possible to prevent the inclusion of permissions by adding some lines to the application’s manifest file. Essentially the lines ask the development tools to "remove" the listed permissions as Gradle is generating the merged manifest.

    android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove"/>
    android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove"/>

Notice that the application is removing both the write and read external storage permissions. This is due to the fact that manifest merging will automatically add the read permission if the write permission was added. You can read more about implicit permissions in the manifest merge guide.

After adding these lines I rechecked the application and everything was back to the explicit list of permissions in the manifest. Of course, you may want to comment why these lines exist to help remind you that you are removing the permission due to an implicit dependency to the maps library.

Update Nov 5, 2015

As of Google Play Services 8.3, the Maps API configuration indicates the WRITE_EXTERNAL_STORAGE permission is no longer required which solves this issue with respect to the Location API. If you are able to update to 8.3 I highly recommend it. This version is also recommended if you are targeting Android 6.0.

The solution would still apply for other 3rd party libraries that may add permissions that are not required for your application.