Objective
Let me take a look at how Iván Carballo and his team used espresso, Mockito and Dagger 2 to write 250 UI tests, and it took only three minutes to run successfully.
In this article, we'll explore how to use Mockito (translator Note: Mockito is a unit test framework written in Java), Dagger 2 to create fast and reliable Android UI tests. This article is worth reading if you are starting to write UI tests in Android, or developers who want to improve existing test performance.
The first time I used a UI Automation test in an Android application was to use Robotium a few years ago: Robotium is an automated test framework in Android. I think the more realistic the test environment is, the better. In the final Test should act like Superman can quickly click on any location and does not give an error, right? I think the mocking test is bad. Why do we need to change the behavior of the application when we test? Isn't that cheating? After a few months we had about 100 test cases that took 40 minutes to run. They are so unstable that even if there are no errors in the functionality of the application, there is usually a chance that the half will fail. We spend a lot of time writing them, but these test cases don't help us find any problems.
But as John Dewey says, failure is instructive.
Failure is instructive. A wise man can always learn as much from failure and success.
We did learn that. We recognize that it is a bad idea to rely on the real API interface in testing. Because you lose control of the returned data, you can't do any up-front processing of your tests. This means that network errors and external API interface errors can cause your test to go wrong. If your wifi goes wrong, you certainly don't want your tests to go wrong. Of course you want the UI test to run successfully. If you're still relying on external API interfaces then you're totally doing integration testing (integration tests) and you don't get the results we expect.
The formal solution to mock tests
(Mocking is the solution)
A mock test is the replacement of a real object with a mock object to facilitate testing. It is primarily used to write unit tests, but is also useful in UI testing. You can emulate Java objects in different ways, but using Mockito is really a simple and effective solution. In the following example you can see a simulated Userapi class and stub (Translator note: Stub, also known as "pile"), which appears mainly in the process of integration testing, as an alternative to the lower program when integrated from the top down. Can be understood as a method of prior processing, to achieve the effect of modification. One of these methods is not translated as follows, so it always returns a static array of username username.
Class Usersapi {
string[] Getusernames () {}
}
//Create The mock version of a Usersapi class
Usersapi MOC Kapi = Mockito.mock (usersapi.class);
Stub the Getusernames () method when
(Mockapi.getusernames ())
. Thenreturn (New string[]{"User1", "User2", " User3 "});
The call below would always return to an array containing the
//three users named above Mockapi.getusernames
();
Once you create a mock object you need to make sure that you use the mock object when you apply the test, and that you are using real objects at run time. This is also a difficult point, if your code is not built to be easy to test (test-friendly), the process of replacing the real object becomes extremely difficult or even impossible to complete. Also note that the code you want to simulate must be separate into a separate class. For example, if you use HttpURLConnection to invoke the rest API directly from your activity to make data access (I hope you don't), this process can be very difficult to simulate.
Consider the system architecture before testing, and poor system architectures tend to make test cases and mock tests difficult to write, and mock tests can become unstable.
An easy to test architecture
A Test Friendly architecture
There are a number of ways to build an architecture that is easy to test. Here I will use the architecture used in Ribot as an example of the Android application architecture mentioned at the beginning, and you can apply this architectural approach to any architecture. Our architecture is based on the MVP model, and we decided to simulate (mock) the entire model layer in the UI test, so we can be more operational with the data and be able to write more valuable and reliable tests.
MVP Architecture
DataManager is the only class in the model layer that is exposed to the presenter layer, so in order to test the model layer we just need to replace it with a simulation
The Datamanger can be.
Using dagger injection to simulate DataManager
Using Dagger to inject a mock datamanager
Once we've identified what we need to simulate, then we should consider how to replace the real object in the test. We solve this problem through Dagger2 (an Android Dependency injection framework), and if you haven't contacted Dagger, I suggest you read the use of Dagger2 for dependency injection "English" before you continue reading. Our application contains at least one Dagger module and component. They are often called applicationcomponent and applicationmodule. Below you can see a simplified version of the class that provides only the Datamanger instance. Of course, you can also use the @inject annotation on the DataManager constructor using the second method. Here I provide a straightforward way to understand. (Translator Note: Here are two classes of ApplicationComponent and Applicationmodule written together to facilitate intuitive understanding)
@Module public
class Applicationmodule {
@Provides
@Singleton public
DataManager Providedatamanager () {return
mdatamanager;
}
}
@Singleton
@Component (modules = applicationmodule.class) Public
interface ApplicationComponent {
DataManager DataManager ();
}
The applied applicationcomponent is initialized in the application class:
public class MyApplication extends application {
applicationcomponent mapplicationcomponent;
Public ApplicationComponent getcomponent () {
if (mapplicationcomponent = null) {
mapplicationcomponent = Daggerapplicationcomponent.builder ()
. Applicationmodule (New Applicationmodule (this))
. Build ();
return mapplicationcomponent;
}
Needed to-replace the component with a test specific one public
void SetComponent (applicationcomponent application Component) {
mapplicationcomponent = applicationcomponent;
}
}
If you've ever used Dagger2, you might have the same configuration steps, and now it's time to create the module and component that you need to use when creating a test.
@Module public
class Testapplicationmodule {
//We provide a mock version of the DataManager using Mockito
@Pr Ovides
@Singleton public
DataManager Providedatamanager () {return
mockito.mock (datamanager.class);
}
}
@Singleton
@Component (modules = testapplicationmodule.class) Public
interface Testcomponent extends applicationcomponent {
//Empty because extends ApplicationComponent
}
The above Testapplicationmodule uses Mockito to provide the simulated Datamanger object, Testcomponent is the ApplicationComponent inheriting class, Testapplicationmodule is used as a module instead of a applicationmodule. This means that if we initialize testcomponent in our application class, we will use the impersonated DataManager object.
Create JUnit, and set testcomponent
Creating a JUnit rule that sets the Testcomponent
To ensure that the testcomponent is set to the application class before each test, we can create JUnit 4 testrule
public class Testcomponentrule implements Testrule {private final testcomponent Mtestcomponen
T
Private final context Mcontext;
Public Testcomponentrule {mcontext = context;
MyApplication application = (MyApplication) context.getapplicationcontext (); Mtestcomponent = Daggertestcomponent.builder (). Applicationtestmodule (New Applicationtestmodule (application)).
Build ();
Public DataManager Getmockdatamanager () {return Mtestcomponent.datamanager ();
@Override public Statement Apply (final Statement base, Description Description) {return new Statement () {@Override
public void evaluate () throws Throwable {MyApplication application = (MyApplication) context.getapplicationcontext ();
Set the testcomponent before the test runs Application.setcomponent (mtestcomponent);
Base.evaluate ();
Clears the component once the tets finishes it would use the default one.
Application.setcomponent (NULL);
}
}; }
}
Testcomponentrule will create the Testcomponent instance object, which will overwrite the Apply method and return a new Statement, the new Statement:
1 set Testcomponent to the Component object of the application class.
2 Invoke the statement evaluate () method of the base class (this is performed at test time)
3 Set the Component field for application to be empty, and let it revert to its original state. We can prevent the interaction between test cases in this way
Through the above code we can get the simulated DataManager object through the Getmockdatamanager () method. This also allows us to give the DataManager object and the method of stub it. It should be noted that this is only valid when the Testapplicationcomponent Providedatamanger method uses @singleton annotations. If it is not specified as a singleton, then the instance object we get from the Getmockdatamanager method will be different from the instance object used by the application. Therefore, we can not stub it.
Writing test Cases
Writing the tests
Now that we have dagger the right configuration, and Testcomponentrule is available, we have one more thing to do, and that is to write test cases. We use espresso to write UI tests. It's not perfect, but it's a fast, reliable Android test framework. We need an app to test before writing test cases. If we have a very simple app, load the username from the rest API and show it to Recyclerview. Then Datamanger will be the following:
Public DataManager {
//loads usernames-a REST API using a retrofit public
single<list<string>> L Oadusernames () {return
musersservice.getusernames ();
}
}
The Loadusername () method uses retrofit and Rxjava to load the rest API data. It returns a single object and sends a string of strings. We also need an activity to display the username usernames to Recyclerview, we assume this activity is called usernamesactivity. If you follow the MVP model you will also have a corresponding presenter but for intuitive understanding, there is no presenter operation.
Now we want to test this simple activity for at least three situations that need to be tested:
1 if the API returns a valid list of user names, then they will be displayed on the table.
2 If the API returns empty data, then the interface will display an "empty list"
3 If the API request fails, the interface will display "Load User name failed"
The following three tests are shown in turn:
@Test public void Usernamesdisplay () {//Stub the DataManager with a list of three usernames list<string> ExPEC
Tedusernames = Arrays.aslist ("Joe", "Jemma", "Matt");
When (Component.getmockdatamanager (). Loadusernames ()). Thenreturn (Single.just (expectedusernames));
Start the activity main.launchactivity (NULL); Check that this three usernames are displayed for (Sting username:expectedusernames) {Onview (Withtext (username)). Ch
Eck (Matches (isdisplayed ())); @Test public void Emptymessagedisplays () {//Stub a empty list when Component.getmockdatamanager (). Loadusername
S ()). Thenreturn (Single.just (Collections.emptylist ()));
Start the activity main.launchactivity (NULL);
Check the empty list message displays Onview (Withtext ("Empty list"). Check (Matches (isdisplayed ())); @Test public void Errormessagedisplays () {//Stub with a single, emits and error when (Component.getmockdatamana Ger (). Loadusernames ()). Thenreturn (Single.error (New RuntimeException ()));
Start the activity main.launchactivity (NULL);
Check the error message displays Onview (Withtext ("Error loading usernames")). Check (Matches (isdisplayed ()));
}
}
Through the code above, we use the Testcomponentrule and the Android official test framework to provide the activitytestrule. Activitytestrule will let us start the usernamesactivity from the test. Note that we use Rulechain to ensure that Testcomponentrule always runs before activitytestrule. This is also to ensure that testcomponent is set in the application class before any activity runs.
You may have noticed that three test cases follow the same build style:
1 set the precondition by when (XXX). Thenreturn (YYY). This is done through the stub loadusernames () method. For example, the precondition for the first test is to have a valid list of user names.
2 The activity is run by main.launchactivity (null).
3 Pass Check (matches (isdisplayed ())), check the display of the view, and show the values expected by the corresponding predecessor condition.
This is a very effective solution that allows you to test different scenarios because you have absolute control over the initial state of the application. If you do not use a mock to write the three use cases above, it is almost impossible to achieve this effect because the real API interface always returns the same data.
If you want to see a complete example of using this test method, you can view the project Ribot Android boilerplate or Ribot app in GitHub.
Of course, there are some flaws in the solution. First of all, the stub is cumbersome before each test. A complex interface may need to have 5-10 stubs before each test. It is useful to move some stubs into the initialization setup () method, but often different tests require different stubs. The second problem is that there is a coupling between the UI test and the potential implementation, which means that if you refactor DataManager, you also need to modify the stub.
However, we have also applied this UI test method in several Ribot applications, which has proved to be beneficial. For example, we have 250 UI tests in one of our most recent Android apps that can run successfully within three minutes. There are also 380 model layer and presenter Layer Unit test.
Well, I hope this article will give you a great help in understanding the UI test and writing better test code.