×
Community Blog Five Writing Skills Effectively Improving Unit Testing Practices

Five Writing Skills Effectively Improving Unit Testing Practices

This article discusses different writing skills that can improve unit testing practices.

By Zhang Cunxi (Yefeng)

1

1. What Is Unit Testing?

Unit Testing means testing a unit. The unit usually refers to a function or class rather than modules and systems in integration testing. The test process of integration testing involves calls across system modules, which is an end-to-end test. Unit testing focuses on the fine granularity of objects to ensure a class or function is executed correctly as expected.

2. Why Do We Write Unit Testing?

As one of the effective means to ensure code quality, the company is actively promoting unit testing. Combined with the practice of single tests, the following benefits of unit testing are summarized below.

2.1 Bug Reduction and Resource Release

2

The figure illustrates two issues:

  • 85% of defects are generated in the code design stage.
  • The later the bug is found, the higher the cost, which increases exponentially.

Unit testing is the lowest type of testing in all testing sessions. It is the first and most important. Most defects are introduced during the coding phase, and the cost of fixing them continues to rise as the software lifecycle progresses. In daily research and development, we write the main process, various boundaries, and abnormal unit testing for functional units before delivery testing, which can help us find defects in the code. Compared with the cost of troubleshooting, locating, fixing, and releasing from testing students or online abnormal feedback, unit testing is cost-effective. Unit testing can ensure code quality, bring us a quality reputation, and reduce the time invested by others and ourselves in fixing low-level bugs. We can allocate energy to other, more meaningful things.

2.2 Protect Code Refactoring

In the face of the old code left over in the project, we all have the urge to overthrow it and start over. However, it has been tested for stability for a long time, and we are worried about problems after refactoring. This is a situation we often encounter. When we want to refactor the long-used code we are unfamiliar with, and there is no sufficient test resource guarantee, the risk of refactoring introduced defects is high.

How do we ensure that refactoring doesn't go wrong? In Refactoring: Improving the Design of Existing Code, Martin Fowler says:

“Refactoring is a valuable tool, but refactoring alone is not enough. Refactoring correctly requires a solid set of tests to help me spot unavoidable omissions. Even if there are tools that can help me automate some refactorings, many refactoring techniques still need to be guaranteed by test collections.”

“In addition to the need to have a sufficient understanding of the workflow and master many design ideas and patterns, unit testing is an effective means to ensure error-free refactoring. When the refactoring is complete and the new code still passes the unit testing, it means that the original correct logic of the code has not been broken, and the original externally visible behavior has not changed. Unit testing gives us the confidence to refactor.”

2.3 Single Test Writing and Code Review (CR)

Unit testing and CR are two effective means to ensure code quality. The timing of our submission of CR lags in the process of R&D delivery. The time point of review students pointing out places to be optimized or repaired is also late. Thus, the risk and cost of repair have increased.

The process of coding unit testing is a self-CodeReview process. In this process, we test the main process, boundaries, and exceptions of the functional unit and examine the specification, logic, and design of the code. It improves the quality and review efficiency of subsequent submission of CR and exposes the problems in advance.

2.4 Easy Debugging and Verification

When there are multiple collaborators in the project, we only need to mock the data of the dependencies according to the agreement. We do not need to wait for the development and deployment of all the dependent application interfaces before debugging, which improves the efficiency and quality of our collaboration. We disassemble the functional requirements. After developing each small function point, we can write and verify unit testing. This habit enables us to get quick verification feedback on the code. At the same time, when developing a complete function, we need to run through all the single test cases of the project, which makes us perceive whether the change of the whole functional requirements will affect the existing business case.

If we can ensure that each class and function can be executed according to the expected business logic through unit testing, the probability of problems in the integrated functional module or system can be reduced. In this sense, unit testing provides strong support for integration testing and system testing.

2.5 Driven Design and Refactoring

When designing and coding, it is difficult to think about all the problems. One of the important criteria for judging code quality is the testability of the code. We can perform a single test of a piece of code. If it is found to be difficult to write, there are a lot of cases to be written, or the current test framework cannot mock dependent objects and needs to rely on other test frameworks with advanced features, we need to look back at the code to see if the coding design is unreasonable, resulting in low testability of the code. This is a positive feedback process, which helps us redesign and refactor in a targeted way.

3. How to Write Unit Testing

3.1 The Construction of Unit Testing Framework

3.1.1 Unit Testing Framework JUnit

JUnit is the most widely used unit testing framework in the Java language for writing and running repeatable automated tests. It includes the following features:

  • Assertion to Test Expected Results
  • Test Tools for Sharing Common Test Data
  • Test Suites for Easy Organization and Running
  • Test Runner for Graphics and Text

Most Java development environments have integrated JUnit as a unit testing tool, and open-source frameworks have corresponding support for JUnit.

3.1.2 Unit Testing Mock Framework

Dependencies in projects are complex. The unit testing Mock framework simulates the dependencies of the tested class and provides the expected behavior and state, so our single test can focus on the tested class without being affected by the complexity of the dependencies.

Here, we discuss the commonly used Mockito and PowerMock, both of which are used as unit testing simulation frameworks to simulate complex dependency objects in applications. Mockito is implemented based on dynamic proxies, and PowerMock adds a class loader and bytecode tampering technology to Mockito, making it possible to Mock private/static/final methods.

For example, the company uses JaCoCo to detect unit coverage. When we use the Mock tool that supports bytecode tampering, this may cause:

  • Test Failure: A conflict is introduced when the Mock tool and JaCoCo modify the bytecode at the same time.
  • Some classes are not covered.

We recommend using Mockito as our unit testing Mock framework for two reasons:

  1. After version 3.4.0, Mockito supports the Mock of static methods. As the Mock tool is integrated by SpringBootTest by default, we recommend using a higher version of Mockito to complete the Mock of static methods.
  2. PowerMock is not advocated. We do not only pursue a single-test coverage rate. When we need to use Mock tools with advanced features, we need to examine the rationality of the code and try to optimize and refactor it to make it more measurable.

3.1.3 Dependency Introduction

3.1.3.1 Add the Maven Dependency of JUnit
  • Springboot Project
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
  • SpringMVC Project
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
3.1.3.2 An Introduction to the Single Test Mock Framework
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.7.0</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>4.7.0</version>
    <scope>test</scope>
</dependency>

3.2 The Naming of the Single Test Method

3.2.1 Unit Testing Class Specification

  • The unit testing class needs to be placed in the test directory of the project (such as xxx/src/test/java).
  • The name of a single test class should start with the name of the tested class and end with Test, such as

ContentService -> ContentServiceTest.

3.2.2 Unit Testing Method Specification

3.2.2.1 The Naming of Test Methods

A good unit testing method name allows us to know the test scenario, intent, and validation expectations quickly. We recommend using should_{expected result}_when_{tested method}_give_{given scenario}.

Example:

@Test
public void should_returnFalse_when_deleteContent_given_invokeFailed() {
    ...
}

Counterexample:

@Test
public void testDeleteContent() {
    ...
}
3.2.2.2 A Single-Test Method to Achieve Layering

If the implementation of the single-test method is layered clearly, it can make the code easy to understand. It can also improve the efficiency of subsequent CR.

Here, we recommend a given-when-then three-segment structure.

Example:

@Test
public void should_returnFalse_when_deleteContent_given_invokeFailed() {
    // given
    Result<Boolean> deleteDocResult = new Result<>();
    deleteDocResult.setEntity(Boolean.FALSE);
    when(docManageService.deleteContentDoc(anyLong())).thenReturn(deleteDocResult);
    when(docManageService.queryContentDoc(anyLong())).thenReturn(new DocEntity());

    // when
    Long contentId = 123L;
    Boolean result = contentService.deleteContent(contentId);

    // then
    verify(docManageService, times(1)).queryContentDoc(contentId);
    verify(docManageService, times(1)).deleteContentDoc(contentId);
    Assert.assertFalse(result);
}

3.3 An Example of the Single Test Method

3.3.1 Code Cases

public class SnsFeedsShareServiceImpl {

    private SnsFeedsShareHandler snsFeedsShareHandler;

    @Autowired
    public void setSnsFeedsShareHandler(SnsFeedsShareHandler snsFeedsShareHandler) {
        this.snsFeedsShareHandler = snsFeedsShareHandler;
    }

    public Result<Boolean> shareFeeds(Long feedsId, String platform, List<String> snsAccountList) {
        if (!validateParams(feedsId, platform, snsAccountList)) {
            return ResponseBuilder.paramError();
        }

        try {
            Result<Boolean> snsResult = snsFeedsShareHandler.batchShareFeeds(feedsId, platform, snsAccountList);
            if (Objects.isNull(snsResult) || !snsResult.isSuccess() || Objects.isNull(snsResult.getModel())) {
                return ResponseBuilder.buildError(ResponseEnum.SNS_SHARE_SERVICE_ERROR);
            }

            return ResponseBuilder.successResult(snsResult.getModel());
        } catch (Exception e) {
            LOGGER.error("shareFeeds error, feedsId:{}, platform:{}, snsAccountList:{}",
                    feedsId, platform, JSON.toJSONString(snsAccountList), e);
            return ResponseBuilder.systemError();
        }
    }

    // Omit code...
}

3.3.2 Unit Testing Code Cases

@RunWith(MockitoJUnitRunner.class)
public class SnsFeedsShareServiceImplTest {

    @Mock
    SnsFeedsShareHandler snsFeedsShareHandler;

    @InjectMocks
    SnsFeedsShareServiceImpl snsFeedsShareServiceImpl;

    @Test
    public void should_returnServiceError_when_shareFeeds_given_invokeFailed() {
        // given
        Result<Boolean> invokeResult = new Result<>();
        invokeResult.setSuccess(Boolean.FALSE);
        invokeResult.setModel(Boolean.FALSE);
        when(snsFeedsShareHandler.batchShareFeeds(anyLong(), anyString(), anyList())).thenReturn(invokeResult);

        // when
        Long feedsId = 123L;
        String platform = "TEST_SNS_PLATFORM";
        List<String> snsAccountList = Collections.singletonList("TEST_SNS_ACCOUNT");
        Result<List<String>> result = snsFeedsShareServiceImpl.shareFeeds(feedsId, platform, snsAccountList);

        // then
        verify(snsFeedsShareHandler, times(1)).batchShareFeeds(feedsId, platform, snsAccountList);
        Assert.assertNotNull(result);
        Assert.assertEquals(result.getResponseCode(), ResponseEnum.SNS_SHARE_SERVICE_ERROR.getResponseCode());
    }
    
}

3.4 The Coding Skills of a Single Test

3.4.1 Mock Dependent Objects

@RunWith(MockitoJUnitRunner.class)
public class ContentServiceTest {

    @Mock
    DocManageService docManageService;

    @InjectMocks
    ContentService contentService;
    
    ...
}
  • MockitoJUnitRunner makes Mockito's annotations effective or uses the initialization method MockitoAnnotations.initMocks(this).
  • Use @Mock to simulate various dependent objects
  • Use @InjectMocks to inject the mocked dependency object into the target test object. Take the preceding code as an example. In a single test, docManageService is injected into the contentService.

We can also use direct initialization or @Spy to simulate the object and then use the Setter method to simulate the injection of the object. Here is a simpler way.

3.4.2 Mock Return Value

3.4.2.1 Mock No Return Value Method
doNothing().when(contentService.deleteContent(anyLong()));
3.4.2.2 Mock Return Value Method
// given
Result<Boolean> deleteResult = new Result<>(Boolean.FALSE);
when(contentService.deleteContent(anyLong())).thenReturn(deleteResult);
3.4.2.3 Execute Real Call of the Method
when(contentService.deleteContent(anyLong())).thenCallRealMethod();
3.4.2.4 Mock Method Call Exception
when(contentService.deleteContent(anyLong())).thenThrow(NullPointerException.class);

3.4.3 Automated Validation

3.4.3.1 Verify the Call of the Dependent Method
// Verify the input parameter of the calling method. Specify "testTagId".
verify(tagOrmService).queryByValue("testTagId");

// Verify that the queryByValue method is called twice.
verify(tagOrmService, times(2)).queryByValue(anyString());
3.4.3.2 Validate Return Values

Validate the return value or exception of the validation method.

// then
Assert.assertNotNull(result);
Assert.assertEquals(result.getResponseCode(), 200);

// Other common assertion functions
Assert.assertTrue(...);
Assert.assertFalse(...);
Assert.assertSame(...);
Assert.assertEquals(...);
Assert.assertArrayEquals(...);

3.4.4 Other Single Test Techniques

3.4.4.1 Using Mockito to Simulate Static Methods
MockedStatic<TagHandler> tagHandlerMockedStatic = Mockito.mockStatic(TagHandler.class);
tagHandlerMockedStatic.when(() -> TagHandler.getSingleCommonTag(anyString())).thenReturn("tag");
3.4.4.2 Handling Mockito Register Static Method Scope

When executing mvn test, if multiple test methods mock the Mockito.mockStatic(TagHandler.class), an error will be reported because static methods are class-level and will be registered multiple times. You can refer to the following two solutions:

1.  Use @BeforeClass and @AfterClass

@BeforeClass Annotated Methods: It is only executed once, the first method executed when running the junit test class.

@AfterClass Annotated Methods: It is only executed once, the last method executed when running the junit test class.

Example:

@RunWith(MockitoJUnitRunner.class)
public class ContentServiceTest {

    @Mock
    DocManageService docManageService;

    @InjectMocks
    ContentService contentService;

    private static MockedStatic<TagHandler> tagHandlerMockedStatic = null;

    @BeforeClass
    public static void beforeTest() {
        tagHandlerMockedStatic = Mockito.mockStatic(TagHandler.class);
        tagHandlerMockedStatic.when(() -> TagHandler.getSingleCommonTag(anyString())).thenReturn("testTag");
    }

    // Omit the test method.

    @AfterClass
    public static void afterTest() {
        tagHandlerMockedStatic.close();
    }

}

2.  Defining Simulation in try-with-resources Construction

@RunWith(MockitoJUnitRunner.class)
public class ContentServiceTest {

    @Mock
    DocManageService docManageService;

    @InjectMocks
    ContentService contentService;

    @Test
    public void should_returnEmptyList_when_queryContentTags_given_invokeParams() throws Exception {
        try (MockedStatic<TagHandler> tagHandlerMockedStatic = Mockito.mockStatic(TagHandler.class)) {
            tagHandlerMockedStatic.when(() -> TagHandler.getSingleCommonTag(anyString())).thenReturn("testTag");

            // The specific implementation of the single-test method is omitted.
            ...
        }
    }

}
3.4.4.3 How to Mock a Chain Call
public T select(QueryCondition queryCondition) throws Exception {
    LindormQueryParam params = queryCondition.generateQueryParams();
    if (Objects.isNull(params)) {
        LOGGER.error("Invalid query condition:{}", queryCondition.toString());
        return null;
    }

    Select select = tableService.select()
            .from(params.getTableName())
            .where(params.getCondition())
            .limit(1);
    QueryResults results = select.execute();
    return convert(results.next());
}

Mockito provides the solution, such as tableService.select().from(params.getTableName()).where(params.getCondition()).limit(1).

Chain Call Solution: Add parameter RETURNS_DEEP_STUBS when mocking objects

@Test
public void should_returnNull_when_select_given_invalidQueryCondition() throws Exception {
    // when
    TableService tableService = mock(TableService.class, RETURNS_DEEP_STUBS);
    when(tableService.select().from(anyString()).where(any()).limit(anyInt())).thenReturn(null);
    Object result = lindormClient.select(new QueryCondition());
            
    // then
    Assert.isNull(result);
}

3.5 Single-Test Generating Plug-In

IDEA has two easy-to-use single-test automatic generated plug-ins: TestMe[1] and Diffblue[2]. TestMe is mainly introduced here. If you have a better plug-in, please recommend it.

1.  Installation: Search for TestMe in the Plguins in the IDEA settings and download and install it

2.  Use: Find the entry in the code button or directly use the shortcut key option + shift + Q:

3

3.  The generated code is listed below:

The automatic generation of plug-ins facilitates the initialization of some code, which can improve the efficiency of single-test writing, but it also has limitations. Single-test name specifications, specific implementation, etc. still need to be improved and supplemented before normal use.

4

4. How to Land Unit Testing

4.1 Value Cognition of a Clear Single Test

Whether it is projects within the company or open-source projects outside the network, few projects have perfect and high-quality unit testing. The reasons for writing a single test were mentioned before, so they will not be repeated here. In the short term, a single test will undoubtedly bring an increase in development workload and duration, but we have to look at the advantages of a single test from the entire iteration cycle. In the end, sticking to unit testing will reduce the number of defects in iterations and shorten the delivery cycle of requirements.

4.2 Incorporating a Single Test into Process Specification

4.2.1 Incorporating Unit Testing into CR Standards

In the past, our CR only focused on the core business code. In most cases, we can point out obvious defects or unreasonable code design in the review, but various conditions of cases, boundaries, and abnormal situations are difficult to review with the naked eye. If the submitted CR contains perfect and high-quality unit testing, the confidence of both the submitting and reviewing parties will be enhanced.

4.2.2 Release Control

After we commit the code, CI can be set up to run unit testing for that branch. In the release process, add single-test-related controls, such as unit testing pass rate and incremental coverage.

5

4.3 Single Test Workload Evaluation

There is no fixed standard for evaluating the workload of unit testing, depending on the complexity of the business logic. In general, if you have not written unit testing before, in the familiar phase, it can be correspondingly increased by 20%-30% based on the requirements of the workload. After being proficient, increasing the requirements of the workload by 10% is sufficient. When business requirements involve a large number of cases, and a single test needs to cover these necessary processes, we can increase some time to ensure high-quality single tests when evaluating the workload.

5. Postscript

Unit testing is easy to understand but difficult to practice. It is easy to change the way of working. The difficult thing is to establish a consistent consensus and recognize the value of unit testing. This is the only way we can effectively land on the ground.

References

[1] https://plugins.jetbrains.com/plugin/9471-testme

[2] https://plugins.jetbrains.com/plugin/14946-diffblue-cover--create-complete-junit-tests-with-ai

0 1 0
Share on

Alibaba Cloud Community

903 posts | 201 followers

You may also like

Comments