Photo by monicore from Pexels

Stepping into Activity tests with ActivityScenarios

Not so long ago the testing team at Google introduced the Android X Test libraries, amongst which we can find ActivityScenario, a new way to test Activities and code related to them. These APIs made it to stable version a few weeks back and are totally safe for use on daily basis.

The main idea of this tool is to drive the state of the Activity so that we can simulate the app passing through the onCreate(), onResume(), and the other lifecycle methods; which helps developers test the flow between these states.

A very peculiar aspect ofActivityScenario and other APIs from the same suite is the possibility of running tests on JVM (using Robolectric) or on a device. The the only caveat of this approach is the need to move all the needed test files in the test or androidTest folder, based on where we want them to run.

Running tests on the JVM using Robolectric will improve massively the execution time. When we instead run tests on a device or emulator, the Activity is launched on the device, making the test more coherent with the real life scenarios.

Note: for the purpose of this post, it does not really matter where the test is run. We will not show how to include the libraries for running the tests in a specific environment, which can instead be found here.

The main difference between ActivityScenario and ActivityTestRule is the control we have over the Lifecycle of the target Activity. ActivityScenario can drive each state of the component with ease, and even decide to restart it and test what happens in that situation.

Introducing the scenario

Imagine we have a Lifecycle-aware component which is part of splash screen. After a timeout, this component would redirect the user to the login screen if they are not logged in, or to the home screen of the app instead.

We use the Lifecycle to stop all the actions we might take if the user decides to exit the app before the countdown is completed. Since we use this API, we need a LifecycleOwner in which we can add the component as observer, and an AppCompatActivity is the easiest way to achieve this.

Component code

The component itself is pretty small, as we can appreciate from its source code:

In the constructor of the SplashScreenManager, we pass the target Activity boundActivity, the IntentFactory and a duration time in milliseconds. We then get the Lifecycle from the boundActivity and add our component as observer.

When we reach the onStart() state of the Activity, we use a coroutine to spawn an asynchronous operation that suspends for the given time and then forwards the user to the next step, closing the boundActivity right after that step.

In case we reach the onStop before the countdown is over, we cancel it and the user is not forwarded to the next step.

The IntentFactory is not really important for the test we want to write, and the only thing that it should matter is that it works in these way:

  • it loads the token, saved in the SharedPreferences for convenience;
  • if such token is not available, it will forward the users to the login screen;
  • if it is instead available, it will open the home screen of the app.

Writing a test case

Now, we know how we expect this component to work, and we want to write tests around it, so we can make sure it works as expected.

As an example, we will write a test that will check the non-logged in scenario, so we expect the login screen to be invoked:

The first thing we do is invoking Intents.init() from the Espresso Intent library. In this way, we can capture any Intent and check their content later on.

We then create the ActivityScenario of a test Activity, created to be able to drive the Lifecycle as it was real.

At this point, such Activity should be in the onResume state, and we invoke a lambda on its main thread, creating the objects we need, included the SplashScreenManager, using the Activity itself, which is passed as the it parameter to the lambda.

Note: we made sure to delete the token, so we would be in the situation in which the login was required.

The next step is to move the activity to the STARTED state, so that our coroutine would run and launch the new screen. We set the waiting time for the test to 0, to avoid any delay or flakiness due to timing.

Once our asynchronous code was run, we use yet another function from the Espresso Intent library, intended() that is a way to understand what it the content of the Intent, more or less like onView() works with View. In this case, we check if the Intent was meant to launch the login screen.

Last but not least, we need to invoke Intents.release() to stop recording the Intents, and close() on our ActivityScenario, so that we free all the resources and we bring our tests to their initial state. Without this steps, our test cases would be in an unexpected (and potentially not replicable) state, which goes against the idea of having stable test suites.

Note: the complete test suite for the component can be found here.

Why should we use it?

Writing small and fast unit tests is definitely the easiest and most reliable way of verifying the code we write, but, as we built a feature, or a screen, we need to verify how the small pieces fit together and within the Android system. Most of the times, we can use integration tests to make sure the different units fit well together, but at some point we find ourselves in a situation where we need to make sure that all the code we wrote works as expected with the platform. In this scenario, being able to drive the Lifecycle of each component and verifying step by step, is definitely one of the best and least painful way to write a test suite, and this is where ActivityScenario becomes an essential tool.

Special thanks to my friends Corey, Daniele, Fabio, and Walter for proofreading this blogpost.