How to do a good job in unit testing in software testing projects

foreword

As mentioned in the book "Unit Testing", learning unit testing should not only stay at the technical level, such as your favorite testing framework, mocking library, etc., unit testing is far more than just "writing tests", you need to keep working hard Maximizing the return on the time invested in unit testing, minimizing the effort you put into testing, and maximizing the benefits that testing provides is not easy.

Just like the problems we encounter in daily development, it is not difficult to learn a language and master a method. The difficulty is to maximize the return on the time invested. Unit test has a lot of basic knowledge and frameworks. You can find a lot of them when you search on Google. There are also many best practice methodologies. This article is not going to discuss these issues, but to discuss how to use unit tests in our daily work. this weapon.

Definition of unit test

What is unit testing? from Baidu

Unit testing refers to checking and verifying the smallest testable unit in software. As for the meaning of [unit], generally speaking, the specific meaning should be determined according to the actual situation, such as a unit in Java refers to a class, etc.

In human terms, unit testing is a test to verify the accuracy of a class. It is different from integration testing and system testing. It is a front-end, developer-led minimal test.

Some scholars have also drawn the following figure after statistics:

85% of defects are generated in the code design stage ;

·  The later the stage of bug discovery, the higher the cost, which increases exponentially.

From this point of view, the writing of unit test code has an extremely important impact on the delivery quality and labor cost.

common mistakes

Waste of time and affect development speed

The development and testing time curves of different projects are different. You need to comprehensively consider the life cycle of your code, your ability to debug, and how much time you usually spend reviewing problematic code. As the project progresses, these times will increase. If you want the code you write to be used forever, and to prevent people from complaining about what you wrote, unit testing is very necessary.

Testing should be the job of testing

Development is the first person responsible for the code, and the person who is most familiar with the code edits unit tests during the design phase, which not only allows you to deliver more confidently, but also reduces the occurrence of test problems. At the same time, your own full-stack capabilities have also improved.

I didn't write the code, I don't understand

We often complain that the old code is difficult to understand, or lacks CR. In fact, the process of writing unit tests is also a process of CR and learning, and has a deep understanding of the main flow, boundaries, exceptions, etc. of the code. At the same time, it is also a process of self-examination of code specifications, logic, and design. I suggest writing unit tests in refactoring and refactoring in writing unit tests, which complement each other.

How to write good unit tests

In terms of methodology, there is the principle of AIR, which will not be felt like air, namely Automatic (automation), Independent (independence), and Repeatable (repeatable).

My personal understanding is:

1. Automatic operation, through CI integration, to ensure that the unit test can run automatically, and to ensure the verification result of the unit test through assert instead of print output. Ensure that unit tests can be run automatically without manual intervention.

2. Unit tests must be independent, cannot call each other, and cannot have a dependent order. Packages are guaranteed to be independent between each test case.

3. It cannot be affected by the operating environment, database, middleware, etc. When writing a unit test, you need to mock out the external dependencies.

In terms of coverage specifications, there are many standards both within Alibaba and in the industry.

The statement coverage rate reaches 70%; the statement coverage rate and branch coverage rate of the core module both reach 100%. --- "Alibaba Java Development Manual"

Single test coverage grading reference

Level1: The normal process is available, that is, when a function inputs the correct parameters, it will have the correct output

Level2: The exception process can throw logic exceptions, that is, when the input parameters are wrong, system exceptions cannot be thrown, but the logic exceptions defined by oneself can be used to notify the upper layer of the calling code of the error.

Level3: Extreme cases and boundary data are available, and the boundary conditions of input parameters should be tested separately to ensure that the output is correct and valid

Level4: The logic of all branches and loops goes through, and there cannot be any process that cannot be tested

Level5: All field verification of output data, for output with complex data structure, ensure that each field is correct

From the above excerpt, both statement coverage and branch coverage have numerical and methodological requirements, so what is the practice in actual work?

In one quarter, the author's comprehensive incremental coverage of the code submitted in the work almost reached 100%. I can talk about my experience and practice.

A single-test coverage rate of about 60% can be easily achieved, but to achieve a coverage rate of more than 95%, it is necessary to cover various code branches and exceptions, and even configuration and bean initialization methods. The time invested is huge, but Diminishing marginal effects. I want to test toString, methods like getter/setter also don't make sense. How much is appropriate, I don't think there is a fixed standard. A high code coverage percentage does not indicate success, nor does it imply high code quality. The part that should be discarded is boldly ignored.

Best Practices

This title is a bit of a title party. There are countless books and ata articles related to unit testing. My so-called "best practice" is some of the pits I have stepped on in the actual work of Ali, or some important points I personally think are important. If there are mistakes, welcome to discuss .

1. Hidden test boundary value

public ApiResponse<List<Long>> getInitingSolution() {
      List<Long> solutionIdList = new ArrayList<>();
      SolutionListParam solutionListParam = new SolutionListParam();
      solutionListParam.setSolutionType(SolutionType.GRAPH);
      solutionListParam.setStatus(SolutionStatus.INIT_PENDING);
      solutionListParam.setStartId(0L);
      solutionListParam.setPageSize(100);
      List<OperatingPlan> operatingPlanList =  operatingPlanMapper.queryOperatingPlanListByType(solutionListParam);
      for(; !CollectionUtils.isEmpty(operatingPlanList);){
          /*
              do something
              */
          solutionListParam.setStartId(operatingPlanList.get(operatingPlanList.size() - 1).getId());
          operatingPlanList =  operatingPlanMapper.queryOperatingPlanListByType(solutionListParam);
      }
      return ResponsePackUtils.packSuccessResult(solutionIdList);
  }

How to write a unit test for the above code?

Naturally, when we write a unit test, we will mock the database query and find out the information. But if the content of the query exceeds 100, because the for loop enters once, it cannot be found by jacoco's automatic coverage. In fact, this boundary case is not covered, and these boundary conditions can only be handled through the developer's habits.

How to deal with these hidden boundary values? Developers cannot rely on integration tests or code CR. They must take this into account when writing unit tests themselves, so as to avoid future maintenance personnel from falling into the trap.

2. Do not use @Transactional and operate real databases in springboot tests

The context of unit testing should be clean, and the original intention of designing transactional is for integration testing (as introduced by spring official website):

Although it is easier to verify the correctness of the DAO layer by directly operating the DB, it is also easy to be polluted by dirty data in the offline database, resulting in the failure of the single test. The author used to encounter a single test code directly connected to the database, and often changed the code for 5 minutes, and cleaned the dirty data in the database for an hour. The second is that the integration test needs to start the container of the entire application, which violates the original intention of improving efficiency.

If you really want to test the correctness of the DAO layer, you can integrate the H2 embedded database. There are many online tutorials, so I won't repeat them here.

3. Time-related content in the single test

The author once encountered an extreme case at work. A CI usually runs normally. Once it was released late at night, the CI failed to run. Later, after checking the next day, it was discovered that someone had taken the current time in the single test. The logic contains night logic (night messages are not sent), which makes CI fail to pass. So how to deal with time in single test?

When using Mockito, you can use mock(Date.class) to simulate the date object, and then use when(date.getTime()).thenReturn(time) to set the time of the date object.

How do you get the current time if you use calendar.getInstance()? Calendar.getInstance() is a static method and cannot be mocked by Mockito. Need to introduce powerMock, or upgrade to mockito 4.x to support:

@RunWith(PowerMockRunner.class)
  @PrepareForTest({Calendar.class, ImpServiceTest.class})   
  public class ImpServiceTest {
      @InjectMocks
      private ImpService impService = new ImpServiceImpl();
      @Before
      public void setup(){
          MockitoAnnotations.initMocks(this);
          Calendar now = Calendar.getInstance();
          now.set(2022, Calendar.JULY, 2 ,0,0,0);
          PowerMockito.mockStatic(Calendar.class);
          PowerMockito.when(Calendar.getInstance()).thenReturn(now);
      }
  }

4. Unit tests for final classes, static classes, etc.

Like the calendar example mentioned in point 3, the mock of the static class needs the version of mockito4.x. Otherwise, powermock must be introduced. Powermock is not compatible with mockito 3.x and mockito 4.x. Since the old application introduced a lot of mockito3.x versions, directly using mockito4.x to mock final and static classes requires packaging. In practice, [url=] JUnit [/url], Mockito, and Powermock have compatibility issues among the version numbers, and java.lang.NoSuchMethodError may occur. You need to choose a version for mocking according to the actual situation.

However, when a new project is established, it is necessary to determine the version of mockito and junit to be used, and whether to introduce frameworks such as powermock to ensure that the environment is stable and available. It is recommended not to change the versions of mockito and powermock on a large scale for old projects, as it is easy to arrange packages and doubt life.

5. The application starts and reports the exception of Can not load this fake sdk class

This is because Ali's tair and metaq are based on the pandora container, and the fake-sdk is loaded by the pandora module class by default. The specific principle can refer to the following figure:

 

Solution 1, introduce the pandoraboot environment.

@RunWith(PandoraBootRunner.class)

This actually slows down the running speed of the single test, which violates the principle of efficiency. But compared to running the entire container, the running time of the pandora container is about 10s, which is still acceptable.

So is there a pure mock method to prevent pandoraboot from booting up? I personally think that mock is more important than ut, especially some external dependencies, which are often migrated or offline, may change 1 line of code, and need to repair 1 hour of test cases. Tair, lindorm and other middleware have no way to mock the local environment, and it is very inelegant to directly rely on external resources.

Solution 2, direct mock

Take tair as an example:

@RunWith(PowerMockRunner.class)
  @PrepareForTest({DataEntry.class})
  public class MockTair {
      @Mock
      private DataEntry dataEntry;
      @Before
      public void hack() throws Exception {
          //solve it should be loaded by Pandora Container. Can not load this fake sdk class. please refer to http://gitlab.alibaba-inc.com/mi ... dora-boot/wikis/faq for the solution
          PowerMockito.whenNew(DataEntry.class).withNoArguments().thenReturn(dataEntry);
      }

      @Test
      public void mock() throws Exception {
          String value = "value";
          PowerMockito.when(dataEntry.getValue()).thenReturn(value);
          DataEntry tairEntry = new DataEntry();
          //值相等
          Assert.assertEquals(value.equals(tairEntry.getValue()));
      }
  }

6. How to write single test in metaq

Refer to 5 for the mock method of MessageExt, but how to run a MetaPushConsumer bean and call the listener method in the single test. Then only the context of context can be started. The way to host SpringRunner.

@RunWith(PandoraBootRunner.class)
  @DelegateTo(SpringRunner.class)
  public class EventProcessorTest {
      @InjectMocks
      private EventProcessor eventProcessor;
      @Mock
      private DynamicService dynamicService;
      @Mock
      private MetaProducer dynamicEventProducer;
      @Test
      public void dynamicDelayConsumer() throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
          //获取bean
          MetaPushConsumer metaPushConsumer = eventProcessor.dynamicEventConsumer();

          //获取Listener
          MessageListenerConcurrently messageListener = (MessageListenerConcurrently)metaPushConsumer.getMessageListener();
          List<MessageExt> list = new ArrayList<>();

          //这个需要依赖PandoraBootRunner
          MessageExt messageExt = new MessageExt();
          list.add(messageExt);
          Event event = new Event();
          event.setUserType(3);
          String text = JSON.toJSONString(event);
          messageExt.setBody(text.getBytes());
          messageExt.setMsgId(""+System.currentTimeMillis());

          //测试consumeMessage方法
          messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
          doThrow(new RuntimeException()).when(dynamicService).triggerEventV2(any());
          messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
          messageExt.setBody(null);
          messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
      }
  }

To summarize when to use containers:

// 1. 使用PowerMockRunner
  @RunWith(PowerMockRunner.class)
  // 2.使用PandoraBootRunner, 启动pandora,使用tair,metaq等
  @RunWith(PandoraBootRunner.class)
  // 3. springboot启动,加入context上下文,可以直接获取bean
  @SpringBootTest(classes = {TestApplication.class})

7. Try to use ioc

Using IOC can decouple objects, making testing easier. It often happens that a certain tool class is used in a certain service, and the methods in this tool class are all static. In this case, when testing the service, it needs to be tested together with the tool class.

For example, the following code:

@Service
  public class LoginServiceImpl implements LoginService{
      public Boolean login(String username, String password,String ip) {
          // 校验ip
          if (!IpUtil.verify(ip)) {
              return false;
          }
          /*
            other func
          */
          return true;
      }
  }

Verify the ip information of the logged-in user through IpUtil, and if we use it in this way, we need to test the method of IpUtil, which violates the principle of isolation. Testing the login method also needs to add more sets of test data to cover the tool class code, and the coupling degree is too high.

If slightly modified:

 @Service
  public class LoginServiceImpl implements LoginService{
      public Boolean login(String username, String password,String ip) {
          // 校验ip
          if (!IpUtil.verify(ip)) {
              return false;
          }
          /*
            other func
          */
          return true;
      }
  }

In this way, we only need to test the IpUtil class and the LoginServiceImpl class separately. When testing LoginServiceImpl, it is enough to mock IpUtil, which isolates the implementation of IpUtil.

8. Don’t test meaningless code for coverage

For example, toString, such as getter and setter, are all machine-generated codes, and single testing is meaningless. If it is for the improvement of the overall test coverage, please exclude this part of the package in CI:

 9. How to test void methods

· If the void method causes changes in the database, such as insertPlan (Plan plan), and the database has been operated through H2, then the change in the number of entries in the database can be verified to verify the correctness of the void method.

·  If the void method calls a function, you can get the number of calls through the verify verification method:

userService.updateName(1L,"qiushuo");
  verify(mockedUserRepository, times(1)).updateName(1L,"qiushuo");

·  If the void method may cause an exception to be thrown.

The exception thrown by the mock method can be mocked by dothrow:

@Test(expected = InvalidParamException.class)
  public void testUpdateNameThrowExceptionWhenIdNull() {
     doThrow(new InvalidParamException())
        .when(mockedUserRepository).updateName(null,anyString();
     userService.updateName(null,"qiushuo");
  }

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/131379002