Disabling animations when running Android instrumented tests

A reliable and extensible approach

Roman Tikonov
3 min readDec 12, 2021
Photo by Isaac Li Shung Tan on Unsplash

Would you like to make your Android instrumented tests, e.g. espresso tests, less flaky and dramatically faster to execute? Then read on.

I will show a reliable and extensible approach to automatically disabling animations on test devices before running instrumented tests and then restoring device animation settings after the test run is over.

Overview

Approach summary:

  1. Replace a standard AndroidJUnitRunner with a custom test runner that extends from it.
  2. Add a test runner delegate that will be triggered at the beginning/end of the test run.
  3. Use Espresso's UiAutomation.executeShellCommand() in a test runner delegate to read and modify device animation settings.

Extensibility note: you can add more delegates for other functionality like automatically unlocking your device before running a test.

Advantages compared to existing approaches I found online:

Step 1: Custom Test Runner

Instrumented tests are executed by a test runner class, which is AndroidJUnitRunner by default. You can probably find this line in your build.gradle:

testInstrumentationRunner androidx.test.runner.AndroidJUnitRunner

AndroidJUnitRunner is an extensible class that offers lifecycle methods like onCreate() or finish(). We will override those methods to invoke our own code, i.e. enable/disable animations, and then configure Gradle to use our custom test runner class instead of AndroidJUnitRunner.

CustomAndroidTestRunner template

Note: If you are setting up a new project without any instrumented tests, check that you have espresso-core dependency in your build.gradle.

Replace the testInstrumentationRunner in your build.gradle with:

testInstrumentationRunner full.package.name.of.your.CustomTestRunner

Step 2: Test Runner Delegates

When writing a custom test runner, let’s follow a structured and extensible approach from the beginning. Instead of putting our own code directly into the test runner, we will put it into separate delegate classes and invoke those classes from the test runner in a unified fashion.

Let’s create a delegate interface and add a list of delegates into the test runner class:

CustomAndroidTestRunner implementation

Note that there is no way to provide constructor arguments to the test runner in Gradle. So we have to provide a delegates list as a default constructor argument.

Step 3: AnimationsTestRunnerDelegate

Now, let’s implement the first delegate that will read device animation settings before the test run, disable animations, and then restore animation settings after the test run is over.

Everything we need is an EspressoUiAutomation object that can execute shell commands on the device. We can achieve it by doing the following:

InstrumentationRegistry.getInstrumentation().uiAutomation
.executeShellCommand("%some command%")

Note: this is an equivalent of invoking “adb shell %some command%” on your laptop’s CLI.

And here are the commands we would need to read/set animations value:

"settings get global animator_duration_scale""settings put global animator_duration_scale $value""settings put global transition_animation_scale $value""settings put global window_animation_scale $value"

By the way, these are the same settings that you see in a developer options menu:

Let’s put it all together in a delegate:

AnimationsTestRunnerDelegate that enables/disables device animations

Note: I have intentionally left out exception handling for clarity of the example.

That’s it. No reflection, no magic, just invoking adb shell settings commands at the right time and place. After you add AnimationsTestRunnerDelegateto your CustomTestRunner, it will do the job whenever you run any instrumented tests from CLI or from Android Studio.

Conclusion

With that approach in place, your Android instrumented tests will run much faster (animations take time) and be more reliable. And you can add your own delegates to the test runner, e.g. to turn on the screen using a WakeLock, or to unlock the device using KeyguardManager.

Cheers!

--

--