Setting animation scale for Android UI tests
The issue
Before testing with Espresso, it is highly recommended to disable animations. Tests may be flaky otherwise.
The simplest (and only officially) way to disable animations is to go manually to Settings app and adjust appropriate settings in Developer options.
That way is very inconvenient when tests are automated e.g. in CI environment. Devices and/or AVDs used there may be reset to factory settings between different jobs. So we have to add some mechanism which automatically disables animations before starting tests.
Existing solutions
The vast majority of the solutions I’ve found on the internet consist of modifying system settings programmatically from an interior of the tests. This approach requires SET_ANIMATION_SCALE permission. Almost all apps don’t need it usually so it must be added to AndroidManifest.xml. Adding it is not enough, it needs to be explicitly granted.
After that preparations we are able to set animation scale. Unfortunately there is no public API for that so reflection needs to be used. You can see that approach eg. in this snippet collection or DeviceAnimationTestRule project. Such huge number of lines of code only to change 3 settings options… Moreover animations may be disabled multiple times in this approach. Even if they was disabled in previous test, a rule or runner disables it again in next one (or at least next test class). That leads to increased test time, overhead may be significant if you have hundreds or thousands of tests.
Theoretically all those disadvantages may be acceptable. There is some boilerplate code and some actions are performed multiple times needlessly but tests works. Well, nothing could be further from the truth. There is an issue which causes random failures while trying to execute shell command using UiAutomator:
1 2 3 4 5 6 | E/UiAutomation: Error executing shell command! android.os.DeadObjectException at android.os.BinderProxy.transactNative(Native Method) at android.os.BinderProxy.transact(Binder.java:615) at android.app.IUiAutomationConnection$Stub$Proxy.executeShellCommand(IUiAutomationConnection.java:418) at android.app.UiAutomation.executeShellCommand(UiAutomation.java:994) |
So we went back to the beginning. Flakiness caused by animations have been transformed to flakiness due to shell command execution… There must be a better way to disable animations!
The better way
Natural solution which comes into mind is to execute ADB shell command from host before running tests. There is a simple command to change the settings: adb shell settings put <namespace> <key> <value>
(NB it was proposed in the already linked gist).
However adb
invoked directly as a shell command on host supports only single device. We can get rid of this limitation by using ddmlib API. It is a part of Android Gradle Plugin which is applied to all Gradle-based Android projects.
Basic solution
First, easiest to implement approach consisted of creating buildscript snippet with custom Gradle task. Here they are:
- groovy-based build.gradle
- kotlin-based build.gradle.kts
Optionally we can add that task as a dependency so that animations will be automatically disabled before running tests. We can also add reverse task which resets animation settings after tests finish. All that is doable in buildscript, however this solution is not so elegant since it requires copy-pasting to each project. Can we do something better? Of course we can!
Final solution
To make solution easily reusable, we can put it into Gradle plugin. Complete source code is available in github repository. Let’s dive into key parts only:
1 2 3 4 5 6 7 8 9 | fun Project.addAnimationTasksWithDependencies() = afterEvaluate { val disableAnimations = createAnimationScaleTask(false) val enableAnimations = createAnimationScaleTask(true) tasks.withType(DeviceProviderInstrumentTestTask::class.java).forEach { it.dependsOn(disableAnimations) it.finalizedBy(enableAnimations) } } |
Here we create tasks which executes appropriate shell commands and add them to the graph. Our task disabling animations becomes dependency of all tasks performing tests on devices. We don’t need to know their names eg. connectedDebugAndroidTest
(which depends on test build type and product flavors) since we search for them by class.
Analogously we declare animation enabling as a finalizer of test task. It will run whenever tests succeed or not. So animation settings on devices will always be restored to default values after tests. Even if there are more test tasks (due to multiple product flavors) each animation scale change will be executed only once – disable before first test and enable after last one.
Finally, we can publish our plugin to Gradle plugin portal. After that it can be applied to any project by just adding one line to buildscript plugins
stanza, like that:
1 2 3 | plugins { id "pl.droidsonroids.animation-disabler" version "1.0.1" } |
Complete plugin source code is available in android-animation-disabler GitHub repository.
About the author
Ready to take your business to the next level with a digital product?
We'll be with you every step of the way, from idea to launch and beyond!
This solution seems like the easiest way to handle this problem, i’ve come across so far. Sadly i never got it to work. I’m using pl.droidsonroids.animation-disabler version 1.0.3, compileSdk 25 and com.android.tools.build:gradle:2.2.3. Any comments/advice? An example project would have been great.
There is currently no example project but example usage is here: https://plugins.gradle.org/plugin/pl.droidsonroids.animation-disabler
One of those snippets need to be inserted.
Note that plugin has to be applied to Android (sub)project not to the gradle root project (which is usually only container for subprojects).
From what you said I guess that you might applied plugin to the root project.
I have basically tried every possible combination and both methods, including adding
classpath “gradle.plugin.pl.droidsonroids.gradle:android-gradle-animation-disabler:1.0.3”
as a dependency in the root build.gradle and applying the plugin in the android (sub)project (named project and module in Android studio). My Espresso tests are failing when animations are enabled doing this, which they are not when i disable animations manually.
Interesting. What is your device model and API level?
Just added sample project and released version 1.0.4 in which you can enable debug logging: https://github.com/koral–/android-animation-disabler#troubleshooting