Unit testing classic three questions: what, why and how?

1, Background

Writing qualified unit tests can be said to be the basic skill of Java programmers. Many companies have requirements for single test coverage, usually ranging from 60% to 90%.

However, many students have more or less conflicts with unit testing, and have questions about how to write "standard" unit testing code.

Some students write unit tests purely to cope with the work, which can not play the role that a single test should play.

This paper answers three basic questions about unit testing, that is, what is unit testing, why to write unit testing, and how to write unit testing?

2, Classic three questions

2.1 what is unit testing?

The English word of Unit Test is Unit Test. What is a Unit?

A unit can be a method, a class, a package or even a subsystem. Unit tests we write during development usually test some or all methods in a class to verify the correctness of their functions.

It is usually used to verify whether a given input can give the expected output.

2.2 why write unit tests?

We know that the sooner mistakes are discovered and solved, the better. Writing unit tests can verify the correctness of the code in the coding stage and correct it as soon as possible.

Unit testing can usually help us find some low-level errors and some logic errors as soon as possible, which is very valuable.

However, in the domestic environment, the construction period is tight, and the preparation of unit tests is time-consuming, so there is often a mentality of coping with tests.

If it is because writing a single test is a waste of time, you can refer to another article of mine to automatically generate some single test codes. You only need to make a few adjustments: Recommendation of Java artifact for automatically generating single test code to bid farewell to overtime / free hands and improve single test coverage

Some students don't recognize the value of unit testing and think that the correctness of code can be verified in the functional testing stage. Writing unit testing is purely dogmatism and a waste of time.

Unit testing is an important means to ensure code quality. (1) Although the function test will be carried out without writing a single test, many problems found in the test stage may be found and solved in the unit test stage. (2) Sometimes, when the amount of data for developing new functions is small, the functional test scenario is not covered, which may bring the errors that could have been found in the unit test stage to the line.

2.3 how to write unit tests?

2.3.1 introduction

Here we only talk about the big logic of unit testing, so that novices can clearly know what to write for a single test, which is not an introductory tutorial for JUnit.

Trilogy of unit testing: given - > when - > then

The so-called given is to construct parameters and conditions (such as mock dependent bean s), and the so-called when executes the target method; then is what we expect after executing the target method under given parameters and conditions.

Here is the sample code:

@Test
public void shouldDeliverCargoToDestination() {
    // given
    Driver driver = new Driver("Teddy");
    Cargo cargo = new Cargo();
    Position destinationPosition = new Position("52.229676", "21.012228");
    Truck truck = new Truck();
    truck.setDriver(driver);
    truck.load(cargo);
    truck.setDestination(destinationPosition);

    // when
    truck.driveToDestination();

    // then
    assertEquals(destinationPosition, truck.getCurrentPosition());
}

Source of sample code: https://blog.j-labs.pl/2017/02/Given-When-Then-pattern-in-unit-tests

In a word: unit testing is similar to the [control variable method] in the experiment. It constructs the interface with known parameters and mock dependence, and asserts whether the running results meet the expectations.

principle: (1) During testing, both normal and abnormal cases should be covered as much as possible. (2) Try to ensure that each branch condition is covered.

2.3.2 cases

Case 1 String splicing tool class

import java.util.Iterator;
import java.util.List;

public class SomeUtils {

    /**
     * Concatenate strings with English commas
     */
    public static  String joinWithDot(List<String> params){

        StringBuilder stringBuilder = new StringBuilder();

        Iterator<String> iterator = params.iterator();
        while (iterator.hasNext()) {
            stringBuilder.append(iterator.next()).append(".");
        }
        return stringBuilder.toString();
    }
}

It is recommended to use the StringJoiner class when splicing strings with actual encoding

We use Recommendation of Java artifact for automatically generating single test code to bid farewell to overtime / free hands and improve single test coverage The introduced Squaretest plug-in automatically generates a single test:

import org.junit.Test;

import java.util.Arrays;

import static org.junit.Assert.assertEquals;

public class SomeUtilsTest {


    @Test
    public void testJoinWithDot() {
        assertEquals("result", SomeUtils.joinWithDot(Arrays.asList("value")));
    }
}

We modify it on this basis:

public class SomeUtilsTest {


    @Test
    public void testJoinWithDot() {
        assertEquals(null, SomeUtils.joinWithDot(null));
        assertEquals("a.b", SomeUtils.joinWithDot(Arrays.asList("a","b")));
        assertEquals("a.null.c", SomeUtils.joinWithDot(Arrays.asList("a",null,"c")));
    }
}

Although it seems that there are only assertions here, such as:

 assertEquals(null, SomeUtils.joinWithDot(null));

This is actually a shorthand, which still conforms to the model of Given when then:

    @Test
    public void testJoinWithDot() {

        // given
        List<String> params = null;
        
        // when
        String result = SomeUtils.joinWithDot(null);

        // then
        assertEquals(null, result);
    }

Run the unit test and find a null pointer:

It indicates that our code is not robust enough and needs to be modified:

 /**
     * Concatenate strings with English commas
     */
    public static  String joinWithDot(List<String> params){
        if(CollectionUtils.isEmpty(params)){
            return null;
        }

        StringBuilder stringBuilder = new StringBuilder();

        Iterator<String> iterator = params.iterator();
        while (iterator.hasNext()) {
           stringBuilder.append(iterator.next()).append(".");
        }
        return stringBuilder.toString();
    }

Run again and report an error:

We assert assertEquals("a.b", SomeUtils.joinWithDot(Arrays.asList("a","b")); The post execution result should be "a.b." but the actual execution result is "a.b."

We continue to modify:

 /**
     * Concatenate strings with English commas
     */
    public static  String joinWithDot(List<String> params){
        if(CollectionUtils.isEmpty(params)){
            return null;
        }

        StringBuilder stringBuilder = new StringBuilder();

        Iterator<String> iterator = params.iterator();
        while (iterator.hasNext()) {
            String each = iterator.next();
            stringBuilder.append(each);
            if (iterator.hasNext()) {
                stringBuilder.append(".");
            }
        }
        return stringBuilder.toString();
    }

Through the unit test, it shows that it meets the expectation.

For the testing of such similar tool classes, you can refer to the unit testing methods of open source projects such as commons Lang and Commons collections 4. https://github.com/apache/commons-lang

https://github.com/apache/commons-collections

Case 2

In normal development, it is more common to test Spring beans, similar to the following:

@Service
public class UserManager {
    @Setter
    private UserDAO userDAO;

    public UserDAO getUserDAO() {
        return userDAO;
    }

    public void setUserDAO(UserDAO userDAO) {
        this.userDAO = userDAO;
    }

    public List<UserDO> someThing(Param param) {

        List<UserDO> result = new ArrayList<>();

        if(param == null) {
            return result;
        }

        List<String> userIds = param.getUserIds();
        if(CollectionUtils.isEmpty(userIds)) {
            return result;
        }

        List<UserDO> users = userDAO.findByIds(userIds);
        if(CollectionUtils.isEmpty(users)) {
            return result;
        }

      return  users.stream().filter(UserDO::getCanShow).collect(Collectors.toList());
    }

}

Note: when the Bean's method is usually relied on,

Continue to use the plug-in one click to generate unit test code:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class UserManagerTest {

    @Mock
    private UserDAO mockUserDAO;

    @InjectMocks
    private UserManager userManagerUnderTest;

    @Test
    public void testSomeThing() {
        // Setup
        final Param param = new Param();
        param.setUserIds(Arrays.asList("value"));
        param.setOthers("others");

        // Configure UserDAO.findByIds(...).
        final UserDO userDO = new UserDO();
        userDO.setCanShow(false);
        userDO.setName("name");
        final List<UserDO> userDOS = Arrays.asList(userDO);
        when(mockUserDAO.findByIds(Arrays.asList("value"))).thenReturn(userDOS);

        // Run the test
        final List<UserDO> result = userManagerUnderTest.someThing(param);

        // Verify the results
    }

    @Test
    public void testSomeThing_UserDAOReturnsNoItems() {
        // Setup
        final Param param = new Param();
        param.setUserIds(Arrays.asList("value"));
        param.setOthers("others");

        when(mockUserDAO.findByIds(Arrays.asList("value"))).thenReturn(Collections.emptyList());

        // Run the test
        final List<UserDO> result = userManagerUnderTest.someThing(param);

        // Verify the results
        assertEquals(Collections.emptyList(), result);
    }
}

We can simply modify the generated single test code according to the business logic:

  @Test
    public void testSomeThing() {
        // given
        final Param param = new Param();
        List<String> userIds = Arrays.asList("value");
        param.setUserIds(userIds);
        param.setOthers("others");


        final UserDO userCanNotShow = new UserDO();
        userCanNotShow.setCanShow(false);
        userCanNotShow.setName("name");

        final UserDO userCanShow = new UserDO();
        userCanShow.setCanShow(true);
        userCanShow.setName("name");
        final List<UserDO> userDOS = Arrays.asList(userCanNotShow, userCanShow);
        when(mockUserDAO.findByIds(userIds)).thenReturn(userDOS);

        // when
        final List<UserDO> result = userManagerUnderTest.someThing(param);

        // then
        assertEquals(1, result.size());
        assertSame(userCanShow, result.get(0));
    }

We only need to write part of the code to complete the unit test, which can save us a lot of time.

If you need mock private methods, static methods, etc., please learn by yourself. You can use it powmock And other tools.

We can also use other tools to automatically generate test parameters or return values. https://github.com/j-easy/easy-random

You can refer to my previous article: Artifact of efficient object construction in Java: an introduction to easy random

One or two lines can construct a very complex object or object list.

Artifact for generating test strings from Java unit tests: Java faker

If we want to randomly construct a string of person's name, place name, weather, school, color, occupation, or even a regular expression

3, Summary

This article briefly introduces what unit tests are, why to write unit tests and how to write unit tests. Hope to help you. If you have any questions, please leave a message and communicate with me.

Posted by brittny85 on Sun, 05 Dec 2021 18:48:04 -0800