Disabling animations when running Android instrumented tests
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.
- Replace a standard
AndroidJUnitRunnerwith a custom test runner that extends from it.
- Add a test runner delegate that will be triggered at the beginning/end of the test run.
- 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:
- It does not require granting permissions and does not rely on reflections as in https://gist.github.com/danielgomezrico/9371a79a7222a156ddad. Also, it does not require running any separate shell scripts.
- It does not require scripting Gradle tasks as in https://product.reverb.com/disabling-animations-in-espresso-for-android-testing-de17f7cf236f
- It does not require UiAutomator dependency as in https://proandroiddev.com/one-rule-to-disable-them-all-d387da440318. Also, it is run once at the beginning of the whole test run, and not on every test class as with a test rule.
- It works for tests run both from CLI and Android Studio, unlike Gradle’s TestOptions.animationsDisabled
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
AndroidJUnitRunner is an extensible class that offers lifecycle methods like
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
Note: If you are setting up a new project without any instrumented tests, check that you have
espresso-coredependency in your build.gradle.
testInstrumentationRunner in your
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:
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 Espresso
UiAutomation object that can execute shell commands on the device. We can achieve it by doing the following:
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:
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
CustomTestRunner, it will do the job whenever you run any instrumented tests from CLI or from Android Studio.
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