Share a summary of unit tests based on Android projects

Foreword:

Responsible for the establishment of the company's unit test system, it has been about a month or two, from the initial framework research, to the mid-term training of all staff, and the introduction and promotion of unit tests for dozens of projects in the later period, it can be regarded as a great contribution to Android. The unit test has some preliminary gains and some new cognitions, so write this article to make a record and summary.

All the following content is purely personal opinion, welcome to discuss.

1. Unit test standard

1. Test dimensions

Unit tests have many dimensions, such as the dimension for function points, or the dimension for methods. So how should we define this dimension for our project?

In the Android source code project, the unit test is written in the dimension of function points, such as verifying the function of sending a broadcast, and the verification is whether the broadcast receiver receives the notification. The process includes four steps: sending the broadcast to the system side, receiving and processing the system side, notifying the application on the system side, and distributing the application to the receiver. For the Android system, after the broadcast is sent, it must be able to ensure that the broadcast is received, but there is a degree of coupling in the unit test, because if there is a problem with the code on the system side, the entire process will not work. For Android projects, we cannot use native objects for many points, but need to use Mock objects. However, as the coupling degree of the project increases and the number of links increases, the objects and verification points that require mocks will increase explosively, resulting in later The cost of the unit test method will increase geometrically.

Of course, taking the function point as the dimension will also have its advantages. If the unit test fails, it means that there must be a problem in one part of the process, and it is easier to expose the problem.

If the method is used as the dimension, there will be no dependency and coupling problems, but the coverage will be much smaller. So should we take the function point as the dimension or the method as the dimension?

In my opinion, this ultimately depends on the structure and form of the project. If it is a UI-level project, the project complexity is relatively light, or a variety of frameworks are used to complete the view binding. This kind of project is naturally suitable for writing unit tests for the dimension of function points. However, if the project coupling degree is relatively high and the complexity is high, you should choose the method-based dimension and use mock objects to cut the coupled parts.

2. Coverage

Function point: core function point

First, unit tests should cover all core functional points. When verifying a method, not all points in the method need to be verified, such as some content in the onCreate method in Activity, such as when writing unit tests for onCreate, verify whether the three methods of initView/initListener/init are executed In fact, it doesn't make sense. We should write unit test code to verify these three methods separately. This is our business logic point.

@Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      initView();
      initListener();
      init();
  }

In addition, we should verify the specific implementation logic method. For some transit methods, we should not verify it. For example, the ApplicationThread class in ActivityThread should not be verified. Some code references in ApplicationThread:

private class ApplicationThread extends IApplicationThread.Stub {
      public final void scheduleReceiver(Intent intent, ActivityInfo info,
          CompatibilityInfo compatInfo, int resultCode, String data, Bundle extras,
          boolean sync, int sendingUser, int processState) {
          updateProcessState(processState, false);
          ReceiverData r = new ReceiverData(intent, resultCode, data, extras,
                  sync, false, mAppThread.asBinder(), sendingUser);
          r.info = info;
          r.compatInfo = compatInfo;
          sendMessage(H.RECEIVER, r);
      }

      public final void scheduleCreateBackupAgent(ApplicationInfo app,
              CompatibilityInfo compatInfo, int backupMode, int userId, int operationType) {
          CreateBackupAgentData d = new CreateBackupAgentData();
          d.appInfo = app;
          d.compatInfo = compatInfo;
          d.backupMode = backupMode;
          d.userId = userId;
          d.operationType = operationType;

          sendMessage(H.CREATE_BACKUP_AGENT, d);
      }
      ...
  }

3. Coverage requirements

Unit tests will have a coverage rate for classes, methods, and lines. Most companies focus on line code coverage and will set their goals at a high goal of 90% or even 95%.

Personally, at least for Android projects, such a high coverage rate is actually not advisable. For example, in the above example, in the unit test class ActivityThreadTest in the Android source code, there is no verification of the content in the ApplicationThread, because this part of the code belongs to the assembly of input data and the distribution of logic, and there is no executable logic. From the perspective of our unit test function, it does not meet any of the functions, so it is meaningless to write unit tests for this kind of code.

So, unit tests should cover all our logic processing code. If I were to do it, I would choose to extract ApplicationThread into a separate class, and mark it as not requiring unit testing to avoid being counted.

In this scenario, I think the line coverage target should be set at 85%. The parts that cannot be covered include the parts that are not convenient to be extracted into separate classes, the parts that define constants, and so on.

4. Naming convention

Unit testing is also writing code, and writing code should have certain specifications.

Referring to the unit test class in the Android source code, it is more appropriate to formulate the naming convention of the unit test according to the following scheme.

1). The unit test class corresponds to the class to be tested, and adding Test after the class to be tested represents its unit test.

For example, if the tested class is ActivityA, the unit test class is named ActivityATest.

2). If the unit test class is based on the method, the corresponding test method is named: test+test method.

For example, if the method to be tested is methodA, the test method is testMethodA (named in camel case).

A test method can cover multiple source methods. Similarly, a source method can also be split into multiple test methods for verification.

3). If the unit test class is based on function points, the corresponding test method is named: test+corresponding function points.

For example, to verify whether the broadcast can be sent to the receiver, its method name is: testResult.

2. The role of unit testing

1. Unit testing cannot effectively improve project quality

I learned that some companies use unit testing to replace integration testing or even black-box testing, but I personally feel that it is not advisable. Perhaps, the scope of the unit tests of these companies has covered part of the functional tests, but the unit tests are after all the method-level function points of the verification. If it is forced to cover the functional tests, it will cause some bad effects.

The unit test guarantees the input and output items in a method. When the project develops new requirements or refactors, the unit test can help us quickly identify the impact points on the original project. This is what the unit test should guarantee.

2. Quickly identify the impact of new features

This is easy to understand. If the new changes affect the original logic in the method, the old unit tests will not work. At this time, we need to judge whether to modify the unit test cases according to the requirements, or whether there is a problem with the newly written logic.

3. Find hidden problems

This is also an unexpected gain during the implementation of unit testing. After entering a certain page, the refreshData method is called twice, but since the logic of the two calls is consistent, it is not known that this method is called twice just by looking at the performance.

However, through unit test verification, whether the verification method execution is 1 at this time, it is verified that the number of times the method is called is not 1 time, and the problem of multiple calls is never found.

//被检测的类型
  public class MVPPresenter implements IMVPActivityContract.IMainActivityPresenter {
      //这个方法被调用了多次
      @Override
      public void requestInfo() {
          //请求数据,订阅,并显示
          Consumer<InfoModel> consumer = this::processInfoAndRefreshPage;
          Flowable<InfoModel> observable = DataSource.getInstance().getDataInfo();
          Disposable disposable = observable
                  .subscribeOn(Schedulers.io())
                  .observeOn(AndroidSchedulers.mainThread())
                  .subscribe(consumer);
      }
  }

  //单元测试类
  public class MVPActivityTest {
      @Test
      public void testInit() {
          ...
          MVPPresenter mockPresenter = mock(MVPPresenter.class);
          ...
          //验证requestInfo方法被调用的此时是否是1次
          verify(mockPresenter, times(1)).requestInfo();
      }
  }

4. Urge us to solve the coupling in the project

Unit testing can be a good measure of project coupling. When I was responsible for building the unit test system of the company's project, I communicated with the person in charge of many projects. Many people said that the unit test case was very difficult to write. After investigation, without exception, all of them are caused by too high coupling. They couple a large number of functions and links that need to be executed separately into one method.

For example, in the onReceive method of BroadcastReceiver, it not only executes the receiving logic of parameters, but also writes the subsequent logical operation logic in the onReceive method, and some even create a new thread here to execute related logic. Such a degree of coupling , the unit test method must be difficult to write. Conversely, if the coupling degree of the project is very low, then the unit test will be easy to write.

5. Objective evaluation of the method

We often refer to a concept called cyclomatic complexity. For measuring the cyclomatic complexity within a method, unit testing is a good indicator. The higher the cyclomatic complexity, the more cases there will be in the unit test code. For that kind of method is very short, but there are many verification cases, its cyclomatic complexity is often very high.

Therefore, by reading the unit test code, you can easily measure its cyclomatic complexity.

6. Supplementary Notes

Some articles call unit testing the best comment, because it can visually display the input and output of a method, which will be more detailed than the method comment. But in my opinion, comments and unit tests should have their own advantages. For a method, the introduction of unit tests is clearer than that of comments, but it also increases our reading cost. So I prefer to call it a supplement to the annotation.

If it's just for reiview, or to have some initial understanding of the project, then the comments are enough.

But if you read the source code to make further transformations on its basis, you must have a deep understanding of the original method, because unit testing is a good tool.

For example, in the Android source code, there are many introductions to the sticky broadcast sendStickyBroadcast in ContextImpl, but in contrast to its unit test method:

public void testSetSticky() throws Exception {
      Intent intent = new Intent(LaunchpadActivity.BROADCAST_STICKY1, null);
      intent.putExtra("test", LaunchpadActivity.DATA_1);
      ActivityManager.getService().unbroadcastIntent(null, intent,
              UserHandle.myUserId());

      ActivityManager.broadcastStickyIntent(intent, UserHandle.myUserId());
      addIntermediate("finished-broadcast");

      IntentFilter filter = new IntentFilter(LaunchpadActivity.BROADCAST_STICKY1);
      Intent sticky = getContext().registerReceiver(null, filter);
      assertNotNull("Sticky not found", sticky);
      assertEquals(LaunchpadActivity.DATA_1, sticky.getStringExtra("test"));
  }

Through the single test code, we can clearly know that a sticky broadcast is a broadcast that allows sending first and receiving it after registration. 

Finally, I would like to thank everyone who has read my article carefully. Reciprocity is always necessary. Although it is not a very valuable thing, you can take it away if you need it:

These materials should be the most comprehensive and complete preparation warehouse for [software testing] friends. This warehouse has also accompanied tens of thousands of test engineers through the most difficult journey, and I hope it can help you! Partners can click the small card below to receive  

Guess you like

Origin blog.csdn.net/OKCRoss/article/details/131416751