27 March 2020 Leave a comment Tech-Help
“How do we make Espresso wait for animations to finish?” you might ask. While Espresso does a good job of waiting for certain things happening on the UI thread, Espresso does not wait for animations to finish. While this does not have to be a problem (we have a bunch of tests which work just fine with having animations enabled), it can cause issues.
Just imagine a button flying over the screen and Espresso tries to click it. If it’s not quick enough, it will miss it.
That’s why it’s mostly recommend to disable animations for testing.
Disabling animations for UI tests
The official Espresso documentation states:
To avoid flakiness, we highly recommend that you turn off system animations on the virtual or physical devices used for testing. On your device, under Settings > Developer options, disable the following 3 settings:
- Window animation scale
- Transition animation scale
- Animator duration scale
This is a quick and easy solution for quickly getting most of your tests up and running.
Pros and cons of disabling animations for UI testing
As stated above, there are some really obvious reasons to disable animations. First of all to make your test setup behave more robustly. Secondly, disabling your animations will reduce the test execution time, which can add up when you have a lot of tests. So no need anymore for Espresso to wait for animations to finish.
But there are some issues with this approach too:
- First of all switching animations off is considered an anti pattern to UI testing. UI testing is the process of testing a product’s graphical user interface to ensure it meets its specifications. The animations happening on the screen are an integral part of the whole user experience and should therefore not be omitted.
- Switching animations off and on again in between your tests or test batches, adds even more complexity to your tests. At least if you decide to disable animations for some parts of your app and enable it for other parts.
- Even though not usually the case, your apps might behave functionally different with animations being disabled
How to disable animations:
A: Manually disabling animations in the device settings of your device (see above)
B: You can use the new flag in testOptions called animationsDisabled:
android {
...
testOptions {
animationsDisabled = true
}
}
BE AWARE: that this will only have an effect when running tests from the command line. This will not work in Android Studio. Here you can find more on How to run tests from the Command Line.
C: Use adb via command line:
# Turn off animations
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0
D: Make use of the AnimationRule:
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.runner.Description
import org.junit.runners.model.Statement
import java.io.IOException
import androidx.test.uiautomator.UiDevice
import org.junit.rules.TestRule
class DisableAnimationsRule : TestRule {
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
// disable animations for test run
changeAnimationStatus(enable = false)
try {
base.evaluate()
} finally {
// enable after test run
changeAnimationStatus(enable = true)
}
}
}
}
@Throws(IOException::class)
private fun changeAnimationStatus(enable:Boolean = true) {
with(UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())){
executeShellCommand("settings put global transition_animation_scale ${if(enable) 1 else 0}")
executeShellCommand("settings put global window_animation_scale ${if(enable) 1 else 0}")
executeShellCommand("settings put global animator_duration_scale ${if(enable) 1 else 0}")
}
}
}
E: Use Cappuccino, an addon for Espresso:
import com.autonomousapps.cappuccino.animations.SystemAnimations;
public class BaseTestClass {
@BeforeClass
public static void disableAnimations() {
SystemAnimations.disableAll(InstrumentationRegistry.getContext());
}
@AfterClass
public static void enableAnimations() {
SystemAnimations.enableAll(InstrumentationRegistry.getContext());
}
}
If you don’t want to disable animations for Espresso
There are several ways how you can make Espresso wait for the animations to finish. All of them have one requirement in common: You need an observable variable which holds the information about whether things in your app are still moving or have finished already. The number one way to deal with this is to use an idling resource. We wrote blog post recently about the disadvantages of idling resources, most noteworthy: They require you to introduce new code into your production code.
If you still keep insisting of running your animations, you might want to look into Cappuccino. It provides an implementation for an idling resource which can be used straight away, so you don’t have to implement your own one:
public class MainActivity extends AppCompatActivity {
private TextView mTextHello;
// Declare your CappuccinoResourceWatcher
private CappuccinoResourceWatcher mCappuccinoResourceWatcher;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Get a reference to a CappuccinoResourceWatcher. This also registers it with Cappuccino
// It will return a no-op version for release builds.
// We pass in 'this' as a convenience. Internally, Cappuccino converts 'this' into a String
mCappuccinoResourceWatcher = Cappuccino.newIdlingResourceWatcher(this);
mTextHello = (TextView) findViewById(R.id.text_hello);
}
@Override
protected void onResume() {
super.onResume();
initViews();
}
private void initViews() {
// This tells Cappuccino that the resoure associated with this Activity is busy,
// and instrumentation tests should wait...
mCappuccinoResourceWatcher.busy();
// This is our custom animation, that Espresso knows nothing about, but
// Cappuccino does!
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
mTextHello.setVisibility(View.VISIBLE);
// ...and this tells Cappuccino that the Activity is now idle, and instrumentation tests
// may now proceed
mCappuccinoResourceWatcher.idle();
}
}, 500 /* delay in ms */);
}
}
Your test class could look similar to this:
public class MainActivityTest {
// optional
@BeforeClass
public static void disableAnimations() {
SystemAnimations.disableAll(InstrumentationRegistry.getContext());
}
// optional
@AfterClass
public static void enableAnimations() {
SystemAnimations.enableAll(InstrumentationRegistry.getContext());
}
@Before
public void setUp() throws Exception {
mActivityTestRule.launchActivity(new Intent());
}
@After
public void tearDown() throws Exception {
Cappuccino.reset();
}
@Rule
private ActivityTestRule mActivityTestRule = new ActivityTestRule<>(MainActivity.class, true, false);
/**
* This test uses the common Espresso idiom for instantiating, registering, and unregistering
* custom IdlingResources, but uses our CappuccinoIdlingResource, instead.
*/
@Test
public void testCappuccinoIdlingResource() throws Exception {
CappuccinoIdlingResource idlingResource = new CappuccinoIdlingResource(mActivityTestRule.getActivity());
Espresso.registerIdlingResources(idlingResource);
onView(withId(R.id.text_hello)).check(matches(isDisplayed()));
Espresso.unregisterIdlingResources(idlingResource);
}
/**
* As a convenience, Cappuccino permits the following idiom, which keeps track of
* CappuccinoIdlingResources in an internal registry, keyed by the name of the object passed in.
* In this case, this is our MainActivity, retrieved by calling {@code mActivityTestRule.getActivity()}.
*/
@Test
public void testCappuccinoIdlingResource2() throws Exception {
Cappuccino.registerIdlingResource(mActivityTestRule.getActivity());
onView(withId(R.id.text_hello)).check(matches(isDisplayed()));
Cappuccino.unregisterIdlingResource(mActivityTestRule.getActivity());
}
}