Disabling Animations in Espresso for Android Testing

When using Espresso for Android automated UI testing, it’s recommended that you disable system animations to prevent flakiness and ensure consistent, repeatable results. The Espresso docs provide a sample of how to disable animations programmatically, but leave out some important details. There is some discussion on that wiki page that provides good insight into solving the problems. Using those comments as a base, after lots of research and experimentation, we found a solution that works well for automatically disabling animations consistently for continuous integration tests.

Disable Animations Rule

First, we reworked the Espresso sample runner and turned it into a simple JUnit4 TestRule:

public class DisableAnimationsRule implements TestRule {
private Method mSetAnimationScalesMethod;
private Method mGetAnimationScalesMethod;
private Object mWindowManagerObject;
public DisableAnimationsRule() {
try {
Class<?> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
Method asInterface = windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
Class<?> windowManagerClazz = Class.forName("android.view.IWindowManager");
mSetAnimationScalesMethod = windowManagerClazz.getDeclaredMethod("setAnimationScales", float[].class);
mGetAnimationScalesMethod = windowManagerClazz.getDeclaredMethod("getAnimationScales");
IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
mWindowManagerObject = asInterface.invoke(null, windowManagerBinder);
}
catch (Exception e) {
throw new RuntimeException("Failed to access animation methods", e);
}
}
@Override
public Statement apply(final Statement statement, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
setAnimationScaleFactors(0.0f);
try { statement.evaluate(); }
finally { setAnimationScaleFactors(1.0f); }
}
};
}
private void setAnimationScaleFactors(float scaleFactor) throws Exception {
float[] scaleFactors = (float[]) mGetAnimationScalesMethod.invoke(mWindowManagerObject);
Arrays.fill(scaleFactors, scaleFactor);
mSetAnimationScalesMethod.invoke(mWindowManagerObject, scaleFactors);
}
}

We use the same sample code to reflectively access the methods required to change the animation values, but instead of having to replace the default Instrumentation object to disable the animations, we just add a class rule to each test class that requires animations to be disabled (i.e., basically any UI instrumentation test) which disables animations for the duration of all tests in the class:

@RunWith(AndroidJUnit4.class)
public class AwesomeActivityTest {
@ClassRule
public static DisableAnimationsRule disableAnimationsRule = new DisableAnimationsRule();

@Test
public void testActivityAwesomeness() throws Exception {
// Do your testing
}
}

Getting Permission

So the rule is set up and a test is ready to run, but you need permission to change these animation values so this will fail with a security exception:
java.lang.SecurityException: Requires SET_ANIMATION_SCALE permission

To prevent this, the app under test must both request and acquire this permission.

To request the permission, simply add as you normally would for any standard permission to your AndroidManifest.xml file. However, since this is only for testing, you don’t want to include this in the main manifest file. Instead, you can include it in only debug builds (against which tests will run) by adding another AndroidManifest.xml file in your project’s debug folder (“app/src/debug”) and adding the permission to that manifest. The build system will merge this into the main manifest file for debug builds when running your tests.

To acquire the permission, you need to manually grant the permission to your app. Since it’s a system level permission, just adding a uses-permission tag will not automatically grant you the permission like other standard permissions. To grant your app the permission, execute the “grant” adb shell command on the device you’re testing on after the app has been installed:

shell pm grant com.my.app.id android.permission.SET_ANIMATION_SCALE

Now you should be able to run your tests and disable animations for each test suite that needs them off and restore them when that suite completes. However, as soon as you uninstall the app your grant is gone and you have to manually grant the permission again for the next run.

That’s whack, yo — let’s automate this.

Automating Permission Grant

In the Espresso wiki discussions, a gist is provided that solves this issue. Since we set the permission for debug builds only, we don’t need the tasks that update the permissions in the manifest and just use the tasks that grant the permission (modified slightly). We found that you need to explicitly set the package ID since the build variable evaluates to the test package ID, not the id of the app under test.

task grantAnimationPermission(type: Exec, dependsOn: 'installDebug') {
commandLine "adb shell pm grant com.my.app.id android.permission.SET_ANIMATION_SCALE".split(' ')
}

tasks.whenTaskAdded { task ->
if (task.name.startsWith('connected')) {
task.dependsOn grantAnimationPermission
}
}

Now the permission will be automatically granted after the app is installed on the currently connected device. However, this presents yet another problem — this will fail if you have multiple devices attached since the adb command needs a target if there is more than one device available.

Targeting Multiple Devices

This gist provides a script that allows you to run a given adb command on each device available. If we save this in the app folder as “adb_all.sh” the task becomes:

task grantAnimationPermission(type: Exec, dependsOn: 'installDebug') {
commandLine "./adb_all.sh shell pm grant com.my.app.id android.permission.SET_ANIMATION_SCALE".split(' ')
}

And there we go. Many hoops to jump through but with all of that set up you can now connect multiple devices and / or emulators and just run “./gradlew cC”. With this set up, Gradle will automatically build your app, deploy it to each device, grant it the SET_ANIMATION_SCALE permission, and run all of your tests with animations disabled as required.

Sign up to discover human stories that deepen your understanding of the world.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Responses (3)

What are your thoughts?