Preface For Android unit testing, just reading this one article is enough. A truly complete analysis, truly from 0 to 1, Junit combines Mockito and Robolectric to implement a comprehensive test from M to V to P, Jacoco scans functions, logic, and lines of code for 100% unit test coverage. Are you still frequently turning the WiFi switch on and off to verify the connected and disconnected states? Or are you still frequently using debug breakpoint setValue to observe the logical judgment of the code for a switch judgment? Or are you still repeatedly comparing UI drafts to verify the correctness of a certain UI copy? You may ask, is it wrong to self-test after writing the code? Of course not, self-testing is a good habit, but as an engineer, you should not just look at the black box test, but design a set of test projects that can let the code test the code once and for all. text First, we start from the Model layer and use specific code to explain in detail how to establish a test project with 100% unit test coverage. Strictly speaking, the Model data layer is responsible for data loading and storage, and exists outside the Android environment, so it does not need the support of the Android SDK. Using Junit combined with Mockito can achieve unit testing with 100% conditional branch coverage. If the Model layer of the project has Android dependencies, it may indicate that the code here needs to be refactored. This is also one of the meanings of unit testing, making the code logic clearer. Another benefit of clearing the Android dependencies of the Model layer is that it makes the test case more efficient. The fastest test case with Android dependencies takes 5 seconds to execute, but for a Model class without Android dependencies, the time to run all cases can be reduced to milliseconds. Therefore, it is still necessary to remove the Android dependencies that are not needed in the Model layer. Code The Model layer test code is as follows:
First, initialize the objects that need to be mocked through the @Mock annotation, and then we need to perform test case analysis on the test class. The WeatherModelmode class is a network request data model, so the core of this model class is the request function. First, analyze the request function. The test points that must be covered are as follows: request parameter verification, request success and correct return code processing logic verification, request success but error code processing logic verification and request failure processing logic verification. At the same time, there is an observer unbinding function in the Model class, so the test case also needs to include the unbinding function processing logic test. Through initResponse, we can simulate the interface return value. Here, the method of reading the Json file is used to make the interface return into a Json data file. Combined with the server-side Swagger document, the server-side interface data simulation can be easily implemented.
For APIs with parameters, the first step is to verify the parameters. You may think that this is a waste of talent, but many bloody tragedies tell us that the smaller the things are, the more likely they are to cause problems, and unit testing helps us solve small problems during the coding period without exposing them to the outside world. To verify the correctness of the parameters, we first need to verify whether the put into the queryMap is correct. For queryMap, we need to verify the correctness of the KV key-value pair. Again, we should nip it in the bud, because queryMap is a private variable. Under normal circumstances, we cannot get its value, and adding a get/set method that is useless to the business to this variable is too deliberate. Our goal is to make the code more robust and have fewer bugs, not to test for testing. How can we test without queryMap parameters? Does unit testing have to go from getting started to giving up? In fact, many things are like this. When you feel that there is no way to solve a problem, it must be that you have not considered it carefully enough. We can get the value of the queryMap object through Java reflection. I will not elaborate on the principle of reflection here. In the testParams method, we first obtain the queryMap object through getDeclaredField, and then we need to obtain the put key. The acquisition of the key puts us into the second problem. You may say, what's so difficult about this, just continue to reflect, but this key is a private static variable, and you can't get the key through normal reflection. At most, you will get an exception. Again, don't give up looking for a solution. In the end, we found that as long as we set the virtual machine not to detect private properties, we can complete the acquisition of private static variables. Don't think that it's just a small parameter, and it's not worth the effort. According to incomplete statistics, there are countless bugs caused by writing too many or wrong letters in the interface key value every day.
Under the premise of ensuring parameter passing, we need to test the interface return status next, first of all, the interface return of success. The role of Mockito.when is to set the expected return result. For example, case testRequestSuccess() tests the situation where the request is successful and the return code is correct, so our expectation for the response is to let it execute the onNext method and return the completely correct interface data we initialized. Mockito.when allows the test code to be executed exactly as we expect. However, this statement must be executed before the method is executed, that is, Mockito.when must be executed before model.request(listener,"Shenyang"); to take effect. Junit provides a rich assertion mechanism. With the help of assert, we can implement tests for various situations. However, for void methods without clear return values, assert seems a bit powerless because it cannot find a standard for assertion. At this time, you need to use the verify method of mockito, which is used to verify whether a method of the mock object is correctly executed. Mockito.verify(listener).showLoading(); is to verify whether the loading progress bar can be displayed normally. ArgumentCaptor is a parameter capture, which can capture the data returned by onNext. Through assertion, we can verify whether the data is correct in the case of success. In the case of successful data, we have a process of converting network data to view data. This conversion method is performed in the convert class. Because we are doing unit testing rather than integration testing, based on the WeatherModel test class, we only need to verify whether the convertData() function is called correctly. The content of the data conversion can be tracked by the unit test of the Convert class.
In the actual development process, the server usually makes different service response codes for different states of the same interface. Although the network request is successful when an abnormal response code is returned, it is different from the normal server response. Therefore, a conditional branch test is required for the abnormal service response code. The test method of testStatusError() is similar to testRequestSuccess(), except that the status simulation value is changed from a successful status to an abnormal status. At the same time, the verification function execution is also changed to the listener's failure method.
Request is an interface. We cannot guarantee that our server will give an accurate response every time we request it. At the same time, we cannot guarantee whether the user's network status is smooth when the user makes a request. Therefore, when designing the Model class, we must also take the abnormal state into consideration and handle the abnormal conditions of the interface. Sometimes we need to create some exceptions ourselves to verify the robustness of our code. Similarly, our test class also needs a special method to ensure abnormal state testing. The difference between the testRequestFail() test method and the successful method is that we first need to mock not the interface data, but an exception, Exception exception = new Exception("exception"); Note that the parameter in this Exception is the exception information, because our fail method has the display of exception information, so this parameter must be added, otherwise e.getLocalizedMessage() will throw NPE. In addition, the expectation of Mockito.when has also changed at this time. This time we expect the function to execute the onError method.
The last case in the Model class is testCancelRequest(). Its function is to unbind the request at the right time. Our network request is asynchronous, which means that when we call the requested activity or fragment destroy, if we do not unbind, there is a risk of memory leak. Of course, the maintainers of Rxjava must have thought of the problems we can think of. Subscription is to facilitate us to unbind Rx at the end of the life cycle. The verification method is very simple. It is still through the verify method to verify whether the unbinding method is executed correctly.
So far, we have completed the full coverage test of the model. Click the run button in front of the test class to see the running status of all test classes. Green means success and red means problems. You can view the problem points that caused the test failure through the log below and correct them. With the help of Jacoco statistical tools, you can see the coverage of unit tests. The reason why we choose to use Jacoco instead of the Coverage that comes with the IDE is that there is a loophole in Coverage in the case of testing & conditional branches, which causes the test that does not reach full coverage to be shown as fully covered. There are not many network resources for Jacoco's AndroidStudio integration, and the integration methods either have potential loopholes or are too cumbersome. After two days of continuous searching, I finally found the simplest integration method in history. You only need to add a Jacoco plug-in to the gradle file of the main project, and gradle will generate a Jacoco Task. Double-click to run to generate an Html coverage report. Run our model test class. From the HTML generated by jacoco, you can see that our model has reached 100% full coverage. In this case, can we assume that the M layer of MVP is ok? Wait, we seem to have missed something. That's right, the data conversion class in the onNext case has not been tested. Let's test the convert class. First, let's take a look at the convert class code:
From the code, we can see that our convert class looks a little strange, but it is not Java code, it is Kotlin. Why should a good Java project be mixed with Kotlin, just for show? Of course not, the function of the data conversion class is to judge the network data and package it into view data. We all know that the judgment of null in Java needs to be nested layer by layer. For example, we need to judge the EnglishScore field in the Score class in the Student class. Our writing is as follows:
This is a multi-layered judgment, but for Kotlin we only need to write Student?.score?.englishScore, which greatly reduces the amount of code. For the features of Kotlin, students who are interested can go to the official website to learn more. Let's return to the unit test. The convert class is a data null judgment class. Its function is to assemble the data and assign the default initial value. Because the data on the server side is uncontrollable, as a mobile phone, we cannot completely rely on the backend brother for the user experience, because letting go of any null data will cause a crash for the App. So our test point is whether this class has achieved the function of assigning a default value when the data is empty and taking the network data value when the data is not empty. Here we take a more representative testTemperature as an example. First, set the simulated WeatherData value to 10D. Because the network data has a value, the network data value, i.e. 10D, will be taken. Assertions can be compared and verified through assertEquals. However, one thing to note is that the double type assertion assertEquals(message, double1, double2) is not available. If it is run directly, the test will fail. The comparison of Double requires an error value. Here, an error value of 0.1D is given. Run again and the test bar turns green. At the same time, we need to test whether viewData is assigned a default value of 0.0 when WeatherData is empty. Similarly, we need to verify each piece of data and package it into view data.
The successful execution of the Convert class marks the successful completion of the test of the Model layer. Now let's take a look at the test of the second-order View layer under the MVP architecture. If we run the UI test directly without the help of the UI test framework, we will not be able to get the expected verification, because we will only get a runtime exception. But we have downloaded the corresponding version of the Android SDK before building the project, why is it still throwing an exception? Why not on a real machine or simulator? Does the IDE only provide us with a development and compilation environment for the project, but not a running environment for the project? Quoting Linus Torvalds' classic RTFSC, let's verify our conjecture little by little through the source code. First, we find the android.jar file corresponding to the SDK, and then find a project to add as library. Taking our most commonly used Activity as an example, the source code is as follows:
We can clearly see that all methods throw RuntimeException ("Stub!") at the same time, which is why our test case cannot be carried out. In order to cope with the current situation that UI unit testing is difficult to advance, Google launched a UI unit testing framework called Espresso. Since it is an official framework, it is relatively complete in the operation of the project and the follow-up of related materials. However, the shortcomings of Espresso are also very obvious. Espresso must be run with the help of an Android emulator or a real machine environment. It is also because it needs to run on an Android device that Espresso runs very slowly, making it even more difficult to combine it with Jenkins for automated construction. This made me ponder, if UI unit testing requires so much effort, is there any need to test it? However, the bug statistics of the iteration soon dispelled my idea of giving up UI and only doing logic testing. The ratio of UI to logic bugs in our mobile phone group during the iteration process can basically reach 5 to 1, which means that most of the problems occur in the view layer. The purpose of unit testing is to reduce bugs, and UI is currently our biggest pain point, so UI unit testing is imperative. After continuous resource searching, I finally found Robolectric, a UI testing framework that can be used without Android devices. Its design idea is to implement a set of JVMs that can run Android code, so as to achieve testing outside the Android environment. Since Robolectric needs to download some necessary dependency packages from oss.sonatype.org, but oss.sonatype.org is a foreign website, the download speed is relatively slow. Here you need to modify the build.gradle file of the entire project and modify mavenCentral() to the proxy of Alibaba Cloud {"http://maven.aliyun.com/nexus/content/groups/public/"}. Robolectric's dependencies are:
To run Robolectric, you need to configure the test class first, as follows:
MyRobolectricTestRunner is a custom configuration file pointing to Alibaba Cloud, BuildConfig is the BuildConfig file of the current model, and sdk is the sdk version used. The reason for specifying the sdk version is that Robolectric needs to download the image resources of the corresponding sdk. The specified version will use the sdk resources that have been downloaded locally. When the test is run for the first time, it will automatically download the relevant files from Alibaba Cloud, and then generate a .m2 folder in the system's C drive. If the download is still slow, you can directly copy the .m2 folder to the relative directory of your computer and use it directly. Robolectric can test almost all Android methods, and it is also very simple to use. For example:
What is achieved is to create an Activity. One line of code can simulate the creation and operation of an activity. One line of code solves the trouble that has always troubled us that we cannot obtain the Android environment. With the Activity object, it feels like all problems can be solved instantly. For example, to test the jump of a page:
After setting the current page and the jump page, Robolectric can help us simulate the Intent we expect. At the same time, ShadowApplicaiton can be used to obtain the actual Intent value after the simulation. Combined with Junit, the Intent can be verified, and then the page jump logic can be verified. TextView is the most commonly used and error-prone UI component in our development process, especially when the designer of the team is very imaginative and has subtle differences in the copywriting of different places. It is very easy for us to type one more or less word, or make a mistake or a similar word. In order to ensure the quality of the product, we have to compare the UI manuscripts over and over again, and observe them word by word, which is simply unbearable. The so-called program is life. Don’t we have this kind of trouble of verifying text in our lives? How do we solve it in life? I remember that many years ago, I would see news about people transferring money to ATMs from time to time. This year, there are very few such news. The reason is that the bank has made a second verification of the bank card number. The test of TextView also uses the method of secondary verification. The first text uses the business code, and the second code uses the test code for verification. If the two are inconsistent, it proves that there is a problem with the text. In this way, the uncertainty of comparing by naked eyes can be effectively avoided, and the program can be verified by the program.
First, call the execution function through view.showTemperature();, find the corresponding TextView component through Id, get the displayed text of TextView through getText, and then verify the string through Junit's asertEquals. If the comparison fails, click to see difference through the Log prompt below to accurately see the difference. Robolectric is also very simple for prompting Tost's test, just need to:
Test the color in Resource:
Test Dialog:
The test points of Dialog need to include the display and hiding of Dialog, the prompt text of Dialog and the text display of button. Because many of them are private variables, some Java reflection is used here to help obtain objects. So far, we have completed the testing of the Model layer and the View layer. Of the three MVP brothers, only the P layer has not been tested. Now let's take a look at how to test the P layer. The P layer, as the link between the M layer and the V layer, plays the role of isolating the direct interaction between the view and the data. Because the P layer only holds the V interface, the P layer can also be extracted into a simple pure Java test. Let's take a look at the test code of the P layer:
Since this is just a sample demo, there is not much business logic. Combined with several simple design patterns, the code of Presenter becomes mostly sequential execution, and verification can be completed through Mockito's verify. It should be explained here that the reason for combining design patterns is that the principle of unit testing is that each conditional branch needs to be guaranteed by a test Case. For multiple branches or even multiple nested branches, it will be more cumbersome and require a lot of repeated code. At the same time, it also increases the probability of missed tests. Appropriate addition of design patterns can make up for this well, and the nested conditional judgments can be deleted, greatly reducing or even deleting conditional judgments. After the unit test after the code is improved, only some simple if/else single branch judgments are tested. The verification method is similar to the test method of the Model layer. With the help of Junit and Mockito, we can easily implement the test of the Presenter layer. In the end, many friends have a rejection attitude towards unit testing, thinking that writing unit tests is a waste of time. In fact, if you also count the time for code debugging, bug fixing and regression testing, you will find that unit testing can actually help us save a lot of time. When writing unit tests, you should code with the mentality of verifying the problem. You must not code with the mentality of completing the task indicators, thinking that it is just an indicator arranged by the leader. Many times when an experienced predecessor arranges you to do something, it is not to ask you to complete something, but to tell you the shortcut from the perspective of a person who has experienced it. The thing is right in front of you. Whoever listens to the words will get it. Jacoco Code Coverage |
<<: The 8th Aiti Tribe Technical Clinic
>>: Summary of Android 7.0 Dynamic Permissions
For entrepreneurs, although mini program developm...
Lin Yongjian's iOS development advanced, star...
Hello, everyone. This is the villager's life....
It has been more than 100 years since nuclear iso...
Everyone must like the idea of building a brand...
With the popularity of Wi-Fi devices such as lapto...
Because of ignorance, people think that "men...
Snakes give people the impression of being cold a...
Review expert: Zhu Guangsi, member of Beijing Sci...
As the Internet enters the second half of its dev...
I read a statement a few days ago that in Go and ...
The 2014 World Cup in Brazil ended an hour ago. Ge...
In the eyes of many marketers , this is an era of...
Today we will continue with the conversion part a...
The following 19 weight loss truths can help you ...