Custom TestRunner

Adds several features over the regular AndroidJUnitRunner:

  • support for multidex apps;
  • support to wait several seconds for views to appear (because of transitions, animations, loaders, api calls, etc.) as found here;
  • support to automatically press ‘Allow’ on runtime permission request dialogs.

Usage

Include/adjust all the files below to use it. Once implemented, you can create tests like this:

@Test
public void someTest() {

    // Allow permission
    allowPermissionsIfNeeded();

    // Wait for button to appear
    onView(isRoot()).perform(waitId(R.id.bt_clickyclicky, Sampling.SECONDS_5));
    ViewInteraction button = onView(
            allOf(withId(R.id.bt_clickyclicky), isDisplayed()));
    button.perform(click());
}

If you don’t want to run tests through Android Studio or you don’t want to do a gradle build just for running the tests, execute this command:

# If you don't want to run tests through Android Studio
# and you don't want to do a gradle build just for testing:
adb shell am instrument -w -r -e debug false -e class com.pixplicity.example.test.MyTest com.pixplicity.example.test/com.pixplicity.example.test.CustomTestRunner

AndroidManifest.xml

In case your app supports min sdk < 18, add this file to your /module/src/androidTest/ folder:

<?xml version="1.0" encoding="utf-8"?>
<manifest
    package="com.pixplicity.example.test.debug"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-sdk tools:overrideLibrary="android.support.test.uiautomator.v18"/>
</manifest>

build.gradle

android {
    ...
    defaultConfig {
        ...
        // Default:
        //testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
        // Default for multidex:
        //testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner"
        // Custom:
        testInstrumentationRunner "com.pixplicity.example.test.CustomTestRunner"
    }
}

dependencies {
    ....
    // UIAutomator allows us to click 'allow' on permission requests during tests
    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
}

The CustomTestRunner.java itself

Adjust the onCreate method in case you’re using Multi-Dex.

package com.pixplicity.example.test;

import android.os.Build;
import android.os.Bundle;
import android.support.multidex.MultiDex;
import android.support.test.espresso.PerformException;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.support.test.espresso.util.HumanReadables;
import android.support.test.espresso.util.TreeIterables;
import android.support.test.runner.AndroidJUnitRunner;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject;
import android.support.test.uiautomator.UiObjectNotFoundException;
import android.support.test.uiautomator.UiSelector;
import android.util.Log;
import android.view.View;

import org.hamcrest.Matcher;

import java.util.concurrent.TimeoutException;

import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static android.support.test.espresso.matcher.ViewMatchers.isRoot;
import static android.support.test.espresso.matcher.ViewMatchers.withId;

public class CustomTestRunner extends AndroidJUnitRunner {

    @Override
    public void onCreate(Bundle arguments) {
        // TODO enable this if app is multidex
        //MultiDex.install(getTargetContext());
        super.onCreate(arguments);
    }

    public static class Sampling {

        public static final long SECONDS_1 = 1000;
        public static final long SECONDS_5 = 5 * 1000;
        public static final long SECONDS_15 = 15 * 1000;
    }

    protected static void allowPermissionsIfNeeded() {
        if (Build.VERSION.SDK_INT >= 23) {
            UiDevice device = UiDevice.getInstance(getInstrumentation());
            UiObject allowPermissions = device.findObject(new UiSelector().text("ALLOW"));
            if (allowPermissions.exists()) {
                try {
                    allowPermissions.click();
                } catch (UiObjectNotFoundException e) {
                    Log.e("test", "There is no permissions dialog to interact with", e);
                }
            } else {
                Log.e("test", "Can't find permissions dialog to interact with");
            }
        }
    }

    /**
     * As found here: http://stackoverflow.com/a/22563297, this class helps slowing down the tests
     * in order to wait for API calls or animations to finish.
     *
     * @param viewId
     * @param millis
     * @return
     */
    public static ViewAction waitId(final int viewId, final long millis) {
        return new ViewAction() {
            @Override
            public Matcher<View> getConstraints() {
                return isRoot();
            }

            @Override
            public String getDescription() {
                return "wait for a specific view with id <" + viewId + "> during " + millis + " millis.";
            }

            @Override
            public void perform(final UiController uiController, final View view) {
                uiController.loopMainThreadUntilIdle();
                final long startTime = System.currentTimeMillis();
                final long endTime = startTime + millis;
                final Matcher<View> viewMatcher = withId(viewId);

                do {
                    for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
                        // found view with required ID
                        if (viewMatcher.matches(child)) {
                            return;
                        }
                    }

                    uiController.loopMainThreadForAtLeast(50);
                }
                while (System.currentTimeMillis() < endTime);

                // timeout happens
                throw new PerformException.Builder()
                        .withActionDescription(this.getDescription())
                        .withViewDescription(HumanReadables.describe(view))
                        .withCause(new TimeoutException())
                        .build();
            }
        };
    }
}