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
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
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
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:
@RunWithannotation. 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
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:
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.
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:
Note: in all my attempts, every time I ran a test from the
sharedTestfolder, 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
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
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.