Picture by Bernard Spragg

Pushing the limits of AndroidX Test

AndroidX Test is the new way of testing code that interacts with the Android framework, and it has the very nice feature of running the same test class on either an emulator (or physical device), or on JVM, through Robolectric. The main difference between the two testing environments is where the test is actually contained. When we want the test to be run on device, the class should go in the androidTest folder, while using Robolectric implies that our files stay in the test folder.

This distinction somehow limits the freedom to run the tests in a separate environment on a time by time basis, as each specific test must be in either androidTest or test folder.

Sharing code

We might already be familiar with the idea of sharing some resources among unit and integration test, via a single folder: sharedTest. We can leverage this idea and move the tests we want to be able to run on different configurations to this folder, so that both test and androidTest source code can see them.

The first step is to create the shared folder manually, at the same level where the other two are, with the same package structure:

Now, we can add a few lines to the build.gradle file of the module; this is the logic to share the folder between the two test types:

A very simple test

As an example on how this solution works, we will use this very simple Espresso test:

Note the @RunWith annotation. This is not mandatory when running this test case as integration test, but we need it when using Robolectric to instruct the framework on how to run the suite.

Our next step is to have the same dependencies declared as both testImplementation and androidTestImplementation, so that we make sure that our test always has all the objects it needs, no matter how it is run. The only exception to this procedure is the Robolectric dependency, which is only needed as testImplementation in order to provide the JVM environment:

Latest versions of these artefacts can be found on Android Developers website.

Now, we could instruct our unit tests to use Android resources in the correct way, by including the framework values and features and returning the default values:

This step might not be necessary , and we should always check what defaults are.

Run configurations

Since we want to run our tests in two different modes, we need to have a configuration that runs them as unit test and another one that runs them on device: this can be done from Android Studio with a few clicks, and our configurations will look like these:

Configuration for Instrumentation tests
Configuration for Unit tests
Note: in all my attempts, every time I ran a test from the sharedTest folder, using the little green play icon close to the class name, Android Studio interpreted it like a androidTest . Also, once I created a JVM configuration and run it, every time I then executed the test from the little play button, it would run on JVM. I am not sure whether this is the expected behaviour or a coincidence, but it was a curious thing to share.

One thing worth remembering is that the two configurations might need a different name, because having them spelled in the exact same way could lead Android Studio to confusion: in our case, we simply added “JVM” to identify the configuration that will run locally from the one that runs on device.

Pushing the limits

The procedure we just explored lets us run the test in both configurations, but if we use a CI this can become a flaw in our test routines: since our class is visible by both unit and instrumentation tests, the test cases contained in the shared folder will be run twice. The first one runs on a JVM when we execute the test task, the second one runs on a real version of Android when we run the connectedTest task.

This can quickly become a problem if our test fails in only one of the two environments, so we need to find a solution that would allow us to run the tests where we need them, but only limit our CI to run them on a specific environment.

In order to do so, we can use an API present in JUnit 4 that lets us group our tests into categories and include or exclude them easily. The first step is to create an empty interface, that will be the name of the category we want to address:

Then, we modify our simple Espresso test by assigning the aforementioned category to it:

The last step consists of excluding the category, identified by its full name, from the Unit testing execution, by adding these lines in our build.gradle file:

Wrapping up

This approach is really new and it still needs some refinements. There are situations in which this path can be really easy and help a lot, not only with Espresso tests, but also with Instrumentation ones and everything that relates to the Android framework.

Anyway, if you want to check out this solution and explore how it works, or maybe give some advice on how to make things better, feel free to reach out the repository on GitHub.