Espresso wait for animations

27 March 2020 Stephan Petzl 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:

  1. 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.
  2. 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.
  3. 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());
    }
}

Like this article? there’s more where that came from.

Leave a Reply