Introduction to Android Unit Testing Android projects in high-speed iterative development often require more reliable quality assurance in addition to black-box testing, which is where unit testing comes in. Unit testing periodically tests the project at the function level, and with good coverage, it can continuously maintain the code logic, thus supporting the project to calmly cope with rapid version updates. Unit testing is a white box testing project established by engineers involved in project development outside of the project code. It is used to execute the target function in the project and verify its status or results. Unit refers to the smallest module of the test, usually a function. The green folder shown in Figure 1 is the unit test project. These codes can detect the correctness of the target code. The unit test code will not be compiled into the APK during packaging. Figure 1 Unit test project location Similar to Java unit testing, Android unit testing is also a white-box project to maintain code logic. However, due to the differences in Android operating environments, the environment configuration and implementation process of Android unit testing are different. Java Unit Testing In traditional Java unit testing, we need to design unit test cases for each function. Figure 2 shows a typical unit test case. Figure 2 Unit test example In the above example, for each branch of the function dosomething(Boolean param), we need to construct the corresponding parameters and verify the results. There are three main target functions for unit testing:
Functions that have no return value, no state change, and no triggering behavior are untestable and should not exist in a project. When there are multiple features mentioned above, this article recommends using multiple cases to verify each feature one by one, or using one case to execute the target function one by one and verify its impact. The principle of constructing test cases is to test cases and functions one-to-one, and achieve condition coverage and path coverage. In Java unit testing, a good unit test needs to ensure that all functions are executed correctly, that is, all boundary conditions are verified, and one test case only tests one function, which is easy to maintain. In Android unit testing, it is not required to cover all functions. Function callbacks in the Android SDK do not need to be tested. Android Unit Testing In Android, the essence of unit testing is still to verify the functionality of functions, and the test framework is also JUnit. In Java, when writing code, you only face classes, objects, and functions. When writing unit tests, you can create an object in the test project and then execute its functions for testing. In Android, when writing code, you need to face components, controls, life cycles, asynchronous tasks, message passing, etc. Although the essence is that the SDK actively executes the functions of some instances, creating an Activity cannot make it execute to the resume state, so framework support other than JUnit is required. The current mainstream unit testing frameworks are AndroidTest and Robolectric. The former needs to run on the Android environment, while the latter can run directly on the JVM, which is faster and can be directly executed periodically by Jenkins without preparing the Android environment. Therefore, our unit testing is based on Robolectric. For some scenarios where the test objects have high dependencies and need to be released, we can use the Mock framework. Android unit test environment configuration Robolectric environment configuration Android unit testing still requires the support of the JUnit framework, and Robolectric only provides the running environment for Android code. If you use Robolectric 3.0, the dependency configuration is as follows:
Gradle's support for Robolectric 2.4 is not as good as 3.0, but all the test frameworks of Robolectric 2.4 are in one package, and there are more reference materials. The author is more accustomed to using 2.4. If you use Robolectric 2.4, you need to configure it as follows:
In the above configuration, this article writes testCompile as androidTest, and the common unit test directory names of Android projects are test and androidTest. There is no functional difference between these two writing methods, except that the Android unit test Test Artifact is different. Test Artifact is shown in Figure 3: Figure 3 Test Artifact In the Gradle plugin, the tasks executed by these two artifacts are somewhat different, but they do not affect the writing and effect of unit tests. Although you can actively configure the project path of unit tests, this article still recommends using the project path and configuration writing corresponding to the Test Artifact. Mock configuration If the target object to be tested has many dependencies, you need to remove the dependencies to avoid making the test case too complicated. Using Robolectric's Shadow is a solution, but a simpler mock framework is recommended, such as Mockito, which can simulate objects and provides some functions for verifying function execution. The Mockito configuration is as follows:
Introduction to Robolectric Robolectric unit test writing structure The unit test code is written in the project's test directory (it may also be androidTest, which will be light green in the project). Unit testing is also a standard Java project, written in classes as file units, and the smallest unit of execution is a function. A test case (hereinafter referred to as a case) is a function annotated with @Test. The class with a case in the unit test is executed by the Robolectric framework, and the annotation @RunWith(RobolectricTestRunner.class) needs to be added to the class. The code structure based on Robolectric is as follows:
In the above structure, the function annotated with @Before will be executed immediately after the class is instantiated. It is usually used to perform some initialization operations, such as constructing network requests and constructing Activities. The function annotated with @test is a unit test case, which is executed by Robolectric. These cases are also functions and can be called in other functions. Therefore, cases can also be reused. Each case is independent and will not affect each other. Even if they call each other, there will be no multi-thread interference. Common Robolectric Usage Robolectric supports unit testing ranging from Activity jumps, Activity display View (including menus) and Fragment to View clicks and touches and event responses. Robolectric can also test Toast and Dialog. For tests that require network request data, Robolectric can simulate the response of network requests. For some objects that Robolectric cannot test, such as ConcurrentTask, you can customize Shadow to test them in reality. The following will focus on the common uses of Robolectric. Robolectric 2.4 simulates network requests Since most Activity interface data of commercial apps is obtained through network requests, and network requests are the primary processing module of most apps, when testing Activities that rely on network data, you can prepare network data in the function marked with @Before to simulate network requests. The code for preparing network requests is as follows:
Since Robolectric 2.4 does not send network requests, it is necessary to create the data returned by the network request locally. The filePath of the above function is the path of the local data file, and setDefaultHttpResponse() creates the Response of the request. After the above function is executed, the unit test project has a network request corresponding to the local data, and the Activity displayed after this function is executed is the Activity with data. In the Robolectric 3.0 environment, unit tests can send real requests and request data. This article still recommends using mocks to construct network requests instead of relying on the network environment. Activity display test and jump test After creating the network request, you can test the Activity. The test code is as follows:
Robolectric.buildActivity() is used to construct an Activity. After the create() function is executed, the Activity will run to the onCreate cycle, and resume() corresponds to the onResume cycle. assertNotNull and assertEquals are assertions in JUnit. Robolectric only provides the operating environment, and logical judgment still needs to rely on assertions in JUnit. Activity jump is an important logic of Android development. The test method is as follows:
Fragment display and switching Fragment is part of Activity. During the process of Robolectric simulating the execution of Activity, if the Fragment adding logic in the tested code is triggered, Fragment will be added to Activity. It is necessary to pay attention to the timing of the Fragment appearing. If the addition of the Fragment in the target Activity is executed in the onResume phase, the Fragment will not appear in the Activity before the Activity is executed by Robolectric in the resume() phase. The method of using Robolectric to actively add Fragments is as follows:
The main body of the startFragment() function is the commonly used code for adding fragments. Switching a fragment is often completed by the code logic in the Activity, which requires a reference to the Activity. Clicking and visual verification of controls
Click verification of a control is to call performClick() and then assert and verify its behavior. For click verification of controls such as ListView that involve Adapter, the writing method is as follows:
Slightly different from controls such as button. Dialog and Toast testing The methods for testing Dialog and Toast are as follows:
The above functions need to be executed after the Dialog or Toast is generated, and can test whether the Dialog and Toast pop up. Introduction to Shadow writing The essence of Robolectric is to simulate and test Android components in the Java runtime environment using Shadow, thereby implementing Android unit testing. For some components that Robolectric does not currently support, you can use custom Shadow to expand the functionality of Robolectric.
In the above example, @Implements declares the Shadow object, @RealObject gets an Android object, and constructor is the constructor of the Shadow. Shadow can also modify the functions of some functions. You only need to add @Implementation when overloading the function. This method can effectively expand the functions of Robolectric. Shadow is to extend the Android object by overloading and initializing the real Android object. The function of the shadowed object is close to that of the Android object, which can be regarded as a repair of the Android object. The custom Shadow needs to be declared in config, and the declaration is @Config(shadows=ShadowPoint.class). Introduction to Mock Writing For some test objects with complex dependencies, you can use a mock framework to remove the dependencies. Mockito is a commonly used framework. For example, to mock an object instance of type List, you can use the following method:
The list object instance obtained is an instance of the List type. If mock is not used, List is actually just an interface, and we need to construct or use ArrayList to instantiate it. Unlike Shadow, Mock constructs a virtual object to decouple the dependencies required by the real object. The object obtained by Mock only has the type of the test object, not the real object, that is, the logic of the real object has not been executed. Mock also has some verification functions that complement JUnit, such as setting the execution result of a function. The following example shows:
The above code defines a return value for the tested function that can replace the result of the real function. When this function is used, this verifiable result will have an effect, thus replacing the real result of the function, thus eliminating the dependence on the real function. At the same time, the Mock framework can also verify the number of executions of the function. The code is as follows:
In some scenarios where network dependency needs to be removed, Mock is often used. For example, the network dependency of the retrofit framework is removed as follows:
In this way, the response of Retrofit can be set by the unit test writer instead of coming from the network, thus eliminating the dependence on the network environment. Using Robolectric to build unit tests in real projects Scope of Unit Testing In Android projects, the objects of unit testing are component states, control behaviors, interface elements, and custom functions. This article does not recommend one-to-one testing of each function. Periodic functions such as onStart() and onDestroy() do not need to be fully covered. Commercial projects often adopt the Scrum model, which requires rapid iteration. Sometimes there may not be much time to write unit tests, so it is no longer required to write unit tests for each function. The unit test cases in this article are mostly derived from a short business logic, and the unit test cases need to verify this business logic. During the verification process, developers can deeply understand the business process, and newcomers can see which logic runs how many functions and which boundaries need to be paid attention to by looking at the project unit test - yes, unit testing needs to have business guidance capabilities like documentation. In large projects, when there is a need to modify the code in the base class, it is often impossible to accurately and quickly know the scope of the impact of the change. In emergencies, the method of creating a subclass to override the parent class function is often used, but this is not a long-term solution. With the support of unit tests with sufficient coverage, running the unit test will know the impact of a function change, and you can safely modify the base class. Meituan’s Android unit test writing process is shown in Figure 4. Figure 4 Meituan Android unit test writing process Unit testing ultimately needs to output documented unit test code to provide good code stability guarantees for online code. Unit testing process In actual projects, unit test objects and pages are one-to-one, and it is not recommended to cross pages. Such unit tests are too coupled and difficult to maintain. Unit testing needs to find the entry of the page and analyze the elements and business logic in the project page. The logic here not only includes the display of interface elements and the behavior of control components, but also includes the processing logic of the code. Then you can create a unit test case list (the list is used to record the scope of unit testing in the project, which is convenient for unit testing management and newcomers to understand business processes). The list records the page of the unit test object, the case logic in the object, and the name, etc. Engineers can start writing unit test code based on this list. Unit testing is a quality assurance project at the engineer's code level. The above process cannot fully cover important business logic and boundary conditions. Therefore, after writing, you need to check the coverage rate, find out the function branch conditions that are not covered in the unit test, and then continue to supplement the unit test case list and add the case to the unit test project code. The unit test of the project is completed until all important branches and boundary conditions of the logic in the planned page are covered. The unit test process is shown in Figure 5. Figure 5 Unit test execution process The result of the above analysis of the page entry is the code in the function marked with @Before, and the subsequent loop is all cases (functions marked with @Test). Unit testing project practice In order to systematically introduce the implementation process of unit testing, this article creates a small demo project as a test object. The function of the demo is to allow users to publish the news they see to the server and browse all the published news. It is a typical self-media application. The development and testing of this demo involves TextView, EditView, ListView, Button and custom View, including network requests, multi-threading, asynchronous tasks and interface jumps. It can provide reference samples for most commercial projects. The project page is shown in Figure 6. Figure 6 Unit test case design First, you need to analyze each page of the App and extract the brief business logic from the page. The extracted business logic is shown in the green circle in Figure 6. Based on this logic, design the unit test case (the function with the @Test annotation). The business logic here not only refers to the business in the requirements, but also includes other code logic that needs to be maintained. Business processes are not allowed to cross pages to avoid increasing the maintenance cost of the unit test case. The unit test case design for the interface in the demo is as follows: Table 1 Unit test case list Next, we need to implement the above case in the unit test project. The minimum number of assertions is a judgment of business logic, not a boundary condition of the code. The real case needs to consider the boundary conditions of the code, such as conditions such as the array is empty. Therefore, the final number of assertions will be greater than or equal to the minimum number of assertions. In the demand business, the minimum number of assertions is also the business condition of the demand. After writing the case, you need to run the unit test and check the coverage report. If the coverage report lacks some logic that is not in the unit test case list but exists in the actual logic, you need to update the unit test case list, add the missing logic, and fill in the corresponding code. The unit test in the project is not completed until all the logic that needs to be maintained is covered. Unit testing is not a black box test for QA, and it is necessary to ensure coverage of the code logic. According to Table 1, the "Publish News" case of the first page can directly call the "Write News" case to meet the condition "2. After writing the news, click the publish button". In the JUnit framework, case (the function annotated with @Test) is also a function. Directly calling this function is not a case and is irrelevant to the case. The two will not affect each other and can be called directly to reduce duplicate code. The second page is different from the first one. A network request is required as soon as it is entered, and subsequent businesses need to rely on this network request. Unit testing should not be overly coupled to a certain condition. Therefore, mock is needed to decouple, directly mock the data obtained from the network request, and verify the page's response to the data separately. Summarize Unit testing is not a project that can generate direct returns, and its operation and coverage cannot directly improve code quality, but the code control it brings can greatly reduce the risk of large-scale collaborative development. Today's commercial App development is all collaborative development in large teams, and new people are constantly joining. Whether they are fresh graduates or have worked for many years, when there is a certain degree of business coupling in the code, there is a certain risk in modifying the code, which may affect the previously hidden business logic, or lose previous patches. If there is a unit test project with high coverage, the impact of the new code on the existing project can be quickly located. Unlike QA acceptance, this impact is at the code level. In the unit test process designed in this article, the unit test case is closely linked to the specific business process of the specific page and the code logic of the business. The unit test, like a technical document, can reflect how many functions a business logic runs and what conditions need to be paid attention to. This is a good way for newcomers to understand the business process and integrate the business at the code level. By looking at the previous unit test case, you can know how many functions the business logic on the page corresponding to the case will execute, as well as the possible results of these functions. References [1] http://robolectric.org [2] https://github.com/square/okhttp/tree/master/mockwebserver [3] http://stackoverflow.com/questions/17544751/square-retrofit-server-mock-for-testing [4] https://en.wikipedia.org/wiki/Unit_testing |
<<: Detailed explanation of Android Bitmap cache pool usage
>>: Android event distribution mechanism source code and example analysis
Figure 1 shows the tree-like Calligonum mongolicu...
Looking at the development of China's televis...
Live streaming sales are very popular. It's m...
Anker is a big player in the Amazon industry. Eve...
In this article, the author will explore the oper...
Beijing time, December 30th news, there have been...
Since WeChat became a work app, the number of fri...
As we all know, the growth of an enterprise is cl...
Today I’m going to talk to my friends about Zhihu...
When we talk about competitive analysis , what ar...
Since the US presidential election began on Novem...
When the "lifeline" on the Holter monit...
Since the beginning of this year, the chip produc...
Even if Google is reorganized into a subsidiary o...
"Hi! Good night this time!" Seven years...