Android: Making Espresso Tests Work With Runtime Permissions

The new Runtime Permissions introduced in Android Marshmallow is a great addition to the platform. However, if your app is targeting SDK version 23 and you have Espresso Tests, it could create some complications.

Likely, your current Espresso tests assume that all permissions have been granted at install time. This is fine when running your tests on devices lower than Marshmallow. However, your tests will fail when running on M devices since none of the permissions are granted by default.

One way to make this work is by granting permissions using adb.

$ adb pm grant com.example.myapp android.permission.<PERMISSION>
or
$ adb install -g com.example.myapp

This would work if you are running your own test infrastructure. However, if you are using an external service, you'll likely to not have the ability to run adb commands.

To start making Espresso Tests work with any device including Marshmallow (or N), we need to make our Permissions code easy to swap.

We should make an interface for the Permissions.

public interface PermissionsModule {
    boolean isLocationGranted(Context context);
    boolean shouldShowLocationPermissionRationale(Activity activity);
    void requestLocationPermission(Activity activity, int requestCode);
}

In the app, we implement the PermissionsModule with the real code:

public class DefaultPermissionsModule implements PermissionsModule  {
    public boolean isLocationGranted(Context context) {
        return ActivityCompat.checkSelfPermission(context, Manifest.permission.            ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
    }

    @Override
    public boolean shouldShowLocationPermissionRationale(Activity activity) {
        return ActivityCompat.shouldShowRequestPermissionRationale(activity,
            Manifest.permission.ACCESS_FINE_LOCATION);
    }

    @Override
    public void requestLocationPermission(Activity activity, int requestCode) {
        ActivityCompat.requestPermissions(activity, new String[]{
            Manifest.permission.ACCESS_FINE_LOCATION}, requestCode);
    }
}

While in the Espresso test module, we implement the PermissionsModule with its mock version.

public class MockPermissionsModule implements PermissionsModule {
    private boolean locationGranted = false;
    @Override
    public boolean isLocationGranted(Context context) {
        return locationGranted;
    }

    @Override
    public boolean shouldShowLocationPermissionRationale(Activity activity) {
        return false;
    }

    @Override
    public void requestLocationPermission(Activity activity, int requestCode) {    }

    public void setLocationGranted(boolean locationGranted) {
        this.locationGranted = locationGranted;
    }
}

The value that will determine if the Location Permission has been granted can be easily changed using setLocationGranted depending on what we need to test.

Before running any test, we need to set MockPermissionsModule as the Permission Module.

MockPermissionsModule permissionsModule = new MockPermissionsModule();
@Before
public void setUp() {
    App.setPermissionsModule(permissionsModule);
}

Inside the test, set the value for locationGranted depending on the test we need to do.

@Test
public void shouldShowLocationGrantedText() {
    permissionsModule.setLocationGranted(true);
    ...
}

@Test
public void shouldShowRequestLocationButton() {
    permissionsModule.setLocationGranted(false);
    ...
}

See sample code for the test here.

We would also want to test if our app works when a user grants or denies a permission. However, Espresso cannot access the System Permissions UI.

This is where UIAutomator comes in. Fortunately, we can mix UIAutomator code with Espresso. For it to work though, the minSdkVersion of our app should be at least 18.

To write tests specifically for devices that handles the Runtime Permissions, we need to annotate our tests with @SdkSuppress.

@SdkSuppress(minSdkVersion = 23)
public class PermissionsTest {
    ...
}

For the Permissions test, we have to use the real implementation of the PermissionsModule. With it, the System Permissions UI will be invoked and we will use UIAutomator to interact with the views.

@Test
public void grantLocationPermission() {
    App.setPermissionsModule(new DefaultPermissionsModule());
    device.findObject(By.res(APP_PACKAGE_NAME,
        "btnRequestLocationPermission")).click();
    UiObject2 btnAllow = device.wait(Until.findObject(
        By.res(INSTALLER_PACKAGE_NAME, "permission_allow_button")), 500);
        MatcherAssert.assertThat(btnAllow.isEnabled(), Is.is(true));
        btnAllow.click();

        UiObject2 tvLocationPermissionGranted =
            device.wait(Until.findObject(By.res(APP_PACKAGE_NAME,
            "tvLocationPermissionGranted")), 10000);

        MatcherAssert.assertThat(tvLocationPermissionGranted,
            IsNull.notNullValue());
    }

You can view the full code for PermissionsTests here.

With this, we can easily create a test when a user denies a permission. We'll have to use the view id "permission_deny_button" instead to interact with the Deny button.

What is missing currently is the ability to test onRequestPermissionsResult. We cannot stub onRequestPermissionsResult in the same way we do with onActivityResult using Espresso Intents.

While Runtime Permissions is a great benefit to the user, it has added an additional complexity to us Android Developers. Espresso and UIAutomator helps us handle this complexity by allowing us to write tests making sure we handle permissions properly.

The full code for this project is available at Github along with my other Espresso samples.