As an Android developer at Wirex, my role is to create finer technology products that allow users to manage their traditional currencies along with cryptocurrencies.
When your app is working with a customer’s money you must always make sure that the in-house flow and product is working as expected. That’s why we’ve spent a lot of time performing various tests on our code base – End-to-End (E2E), Unit, User Interface (UI), and so forth.
The most valuable test for us was always E2E, because they can test specific flows from start to finish. For example – single screen test, we mock the server responses and validate that the screen UI is in correct condition. Alternatively, flow test – simulate user interactions (input fields needed), and check that the UI is looking as it should at each step, and finish by collecting the completed information to check that the API calls are correct .
From the outset, all our E2E tests worked using Robolectric. However, we later decided to extend them to run on a simulator as well, which could produce more complex reports with screenshots or other additional information about how it runs on a device. After the tests were done, we realized that there were some tests that passed with Robolecrtic, but failed with some bugs on the simulator.
In this post I want to share some information about the problems we faced and the solutions we found.
Problem 1: Common code
In our case – almost all Robolectric test passcodes could be applied to the instrumentation tests – the same calls, check, and so forth. That’s why we started to create solutions that would reuse common code in both types of tests.
In the following simple example, I want to show the steps that have been taken to separate and reuse common code – a test for verifying visual state changes.
There are two tests for checking this – using Robolectric Instrumentation and Android.
As you can see – all the logic is the same for all types of tests. Therefore, you can create some “main” tests for this case, and reuse it for Roboelectric operations and instrumentation. You need to create a directory in app / src, where you install all common parts. In this example, I named it as sharedTest.
It’s a good idea to do a package structure inside sharedTest, as in other test folders:
Next – you need to provide this folder as a source set for both types of test – Instrumentation and Robolectric.
After that you can create a common test class, where a common logic is given:
Note: There is no need to use @RunWith or @Test annotations in a common test class.
And now you can simplify 2 more tests
Well, this is already a good result – common code is reused, and you have a clear testing structure. However, if you are creating a test with a more complex logic than our example – you will face actions and features that require different operations for Instrumentation and Robolectric tests.
Problem 2: Specific type test code
Let’s get this test a little closer to the real cases. Imagine we need to do some preparation, and as the first step of our test, this preparation should use a certain logic for Robolectric Testing instrumentation (for example InstrumentationRegistry for Instrumentation and Shadow for Robolectric). The main problem here is that if you want to make a clear module and package structure, an ordinary test class should not use specific classes for one of the test type. At Wirex we achieved this by separating construction.gradles files for Robolectric and Instrumentation tests, and we can’t even import, for example, Shadow into a common test class.
According to build.gradle separation, you need to define the difference in appropriate test classes. This goal can be achieved using different methods, and we will look at each of them, from easy (and least functional and extensible) to complex.
Factory Method Pattern
You can solve this problem using the factory method pattern.
After that you need to define a specific logic in both operations.
This solution makes it possible to resolve differences in a straightforward way, but it results in an unclear structure, lots of boilerplate code (if differences are resolved in more than one test) and difficult to support or update them.
Service locator pattern
Another way you can deal with this is to move specific logic to classes and provide it in your common test class using a simple service locator pattern implementation.
First, you need to create an interface for the preparation assistant class. This interface is used in the common test class which is why it should be placed in the sharedTest folder:
After that, create operations for different types of tests. InstrumentationPreparationHelper it should be placed in the androidTest directory a RobolectricPreparationHelper in the test directory.
The next step is to create a service deployment class that will provide its need PrepareHelper action. Insofar as classes are sharedTest with no idea of other test folders and their classes, there is no way to provide operations without using reflection on this. For example, you can find a similar method in the UiControllerModule Module class from the Espresso package.
This class should also be placed in the sharedTest folder to make it possible to use the locator in common test classes.
Note: you need to try to find the Robolectric class first, because classes of androidTest it can also be created for Robolectric testing.
Then you need to update the common test class and its operations to use it SharedTestsServiceLocator:
In implementing this, a different preparation code is implemented depending on how the test runs. Using this method of solving the problem of difference is quite simple and quick to code, but it is also a bit dirty. First of all, if the reference for your helper classes is changed, there is no way to change it inside the service locator automatically. Also, because integrated development environments (IDE) perceive your helper class as unused, that could also lead to some unexpected problems and situations.
Dependency Injection (DI) and Service Locator
To reduce your use of Service locators and to make the codebase more flexible, clear and readable, you can update your logic using the DI pattern with Dagger2.
First of all, you need to define TestApplicationComponent– the interface for Dagr in the future @ Component. In real cases, some components may require an App or Context, so it’s a good idea to create as well TestApplication class, where you create your components and pass the required dependencies to them. Also you need some classes to provide different implementations of TestApplicationComponent. In the example below, this class is TestAppComponentProvider.
- Classes from this file are used in the common test class and should be placed in the sharedTest directory.
- TestApplicationComponent not Dagr @Component, it is just a common interface for real components, which makes it possible to call the spray () method of operating components from a common test class (where there is no knowledge of component operation).
- In the list above you can see the use of SharedTestsServiceLocator (with reflection) at TestApplication provide TestAppComponentProvider – this is the only place you have to use.
- To Use TestApplication in tests you should create a custom TestRunner, where you define which Application class to use for both test variants.
The next step is that you need to define the TestApplicationComponent a TestAppComponentProvider operations for the instrumentation and Robolectric testing. You also need to add @Module to provide the necessary implementation of PrepareHelper.
Update service locator to provide need TestAppComponentProvider:
Finally, you can update the common test class. The code for Robolectric instrumentation and operations does not change.
After this update you are reducing the size SharedTestsServiceLocator just provide the needed TestAppComponentProvider. If you need several different classes in the future, you’ll just be using the current DI system, not using reflection. This method is more readable, clear and extendable.
In this article we created a system for running the instrumentation and Robolectric tests and improved this system step by step to maximize readability and extensibility. This is not the easiest way, as the implementation described requires a day to two days, but if you spend this time at the beginning, you will be able to easily extend your future test system.
Written by Oleksandr Hrybuk, Android Developer
Credits to the Wirex Android Team, Alexander Shaubert and Andrey Derkach for helping me write this article