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();
}
};
}
}