Unit Testing is the Testing of the correctness of the smallest testable units of software or a project. A unit is an artificially defined minimum testable module of functionality. It can be a module, a function, or a class. Unit tests need to be tested in isolation from module development.

After the program development is completed, we often cannot guarantee that the program is 100% correct. By writing unit tests, we can define our input and output programs through automated test programs, Check the results of each Case through assertions, and test our programs. In order to improve the correctness, stability, reliability of the program, save the program development time. The main unit testing frameworks we used in the project were Spring-boot-test TestNG, PowerMock, etc.

TestNG is a Testing framework that uses annotations to enhance Testing capabilities based on JUnit and NUnit. It can be used for both unit and integration Testing.

PowerMock is also a unit test simulation framework that is an extension of other unit test simulation frameworks. By providing custom classloaders and the use of bytecode tampering techniques, PowerMock provides support for emulation of static methods, constructors, private methods, and Final methods, as well as removal of static initialization procedures.

Commonly used annotations

1. The TestNG annotation

  • @beforeSuite Runs only once before all tests of the suite are run in annotated methods
  • AftereSuite only runs once after all tests in the suite are run in the comment method
  • @beforeClass runs before calling the first test method of the current class, annotated methods run only once
  • @Aftereclass is run after the first test method of the current class is called, and the annotated method is run only once
  • The @beforeMethod annotation method will run before each test method
  • The @AfterMethod annotation method will run after each test method
  • The @beforeTest annotated method will run before all test methods of the class belonging to the test tag run
  • The @Aftertest annotated method will run after all test methods of the class belonging to the Test tag have been run
  • DataProvider marks a method to provide data for a test method. An annotated method must return an Object [] [], where each Object [] can be assigned to a list of parameters for the test method. The @test method that receives data from the DataProvider needs to use the DataProvider name equivalent to the annotation name
  • @parameters describes how to pass Parameters to the @test method; Applicable to XML parameterized value transmission
  • @test marks a class or method as part of a Test. If this tag is placed on a class, all public methods of that class will be treated as Test methods

2. PowerMock annotation

  • The @mock annotation is actually short for the mockito.mock () method, and we only use it in our test classes;
  • @InjectMocks actively injects existing mock objects into beans by name, but does not throw an exception if the injection fails;
  • @spy encapsulates a real object so that you can track and set its behavior just like any other mock object.

The sample code

  1. Add the POM.xml dependency

Using the spring-Boot project as an example, we first need to add the TestNG + ProwerMock dependency as follows:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>${testng.version}</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>${powermock.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>${powermock.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-testng</artifactId>
    <version>${powermock.version}</version>
    <scope>test</scope>
</dependency>
Copy the code
  1. Adding unit tests

Add test code

import com.test.testng.dto.OrderDto;
import com.test.testng.dto.UserDto;
import org.mockito.*;
import org.powermock.modules.testng.PowerMockTestCase;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;

public class OrderServiceTest extends PowerMockTestCase {

    @BeforeMethod
    public void before(a) {
        MockitoAnnotations.openMocks(this);
    }

    @InjectMocks
    private OrderService orderService;
    @Mock
    private UserService userService;

    // Normal test
    @Test
    public void testCreateOrder(a) {
        //1. mock method start
        UserDto userDto = new UserDto();
        userDto.setId(100);
        when(userService.get()).thenReturn(userDto);

        //2. call business method
        OrderDto order = orderService.createOrder(new OrderDto());
        //3. assert
        assertEquals(order.getId(), 100);
    }

    // Exception test
    @Test
    public void testCreateOrderEx(a) {
        //1. mock method start
        when(userService.get()).thenThrow(new RuntimeException());

        Exception exception = null;
        try {
            //2. call business method
            orderService.createOrder(new OrderDto());
        } catch (RuntimeException e) {
            exception = e;
        }
        //3. assertassertNotNull(exception); }}Copy the code

Common Mock

Mock static methods

// Static method
UserDto dto = new UserDto();
dto.setId(100000);
PowerMockito.mockStatic(UserService.class);
PowerMockito.when(UserService.loginStatic()).thenReturn(dto);
UserDto userDto = UserService.loginStatic();
assertEquals(100000, userDto.getId().intValue());
Copy the code

Mock private attributes

// Field assignment
ReflectionTestUtils.setField(orderService, "rateLimit".99);
Copy the code

Mock private methods

// Simulate private methods
MemberModifier.stub(MemberMatcher.method(UserService.class, "get1")).toReturn(new UserDto());
// Test private methods
Method method = PowerMockito.method(UserService.class, "get1", Integer.class);
Object userDto = method.invoke(userService, 1);
assertTrue(userDto instanceof UserDto);
Copy the code

Use the advanced

1. Parameterized batch test

When there is a lot of Test data, we can generate data source through @dataProvider and use data through @test (DataProvider = “XXX “), as shown below:

import com.test.testng.BaseTest;
import com.test.testng.dto.UserDto;
import org.mockito.InjectMocks;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import static org.testng.Assert.assertFalse;
import static org.testng.AssertJUnit.assertTrue;

public class UserServiceTest2 extends BaseTest {

    @InjectMocks
    private UserService userService;

    // Define the data source
    @DataProvider(name = "test")
    public static Object[][] userList() {
        UserDto dto1 = new UserDto();

        UserDto dto2 = new UserDto();
        dto2.setSex(1);

        UserDto dto3 = new UserDto();
        dto3.setSex(1);
        dto3.setFlag(1);

        UserDto dto4 = new UserDto();
        dto4.setSex(1);
        dto4.setFlag(1);
        dto4.setAge(1);

        return new Object[][] {{dto1, null}, {dto2, null}, {dto3, null}, {dto4, null}};
    }

    // Correct scene
    @Test
    public void testCheckEffectiveUser(a) {
        UserDto dto = new UserDto();
        dto.setSex(1);
        dto.setFlag(1);
        dto.setAge(18);
        boolean result = userService.checkEffectiveUser(dto);
        assertTrue(result);
    }
    
    // Error scenario
    @Test(dataProvider = "test")
    public void testCheckEffectiveUser(UserDto dto, Object object) {
        booleanresult = userService.checkEffectiveUser(dto); assertFalse(result); }}Copy the code

3. Complex judgment ensures test coverage

Case study:

Identify valid users: age > 18 and sex = 1 and flag = 1

public boolean checkEffectiveUser(UserDto dto) {
    // Determine valid user: age > 18 and sex = 1 and flag = 1
    return Objects.equals(dto.getSex(), 1) &&
        Objects.equals(dto.getFlag(), 1) && dto.getAge() ! =null && dto.getAge() >= 18;
}

Copy the code

Split logic. Convert this to the simplest if… The else statement. Then add the unit test as follows:

public boolean checkEffectiveUser(UserDto dto) {
    if(! Objects.equals(dto.getSex(),1)) {
        return false;
    }
    if(! Objects.equals(dto.getFlag(),1)) {
        return false;
    }
    if (dto.getAge() == null) {
        return false;
    }
    if (dto.getAge() < 18) {
        return false;
    }
    return true;
}
Copy the code

After the split, we can see that we only need 5 unit tests to achieve full coverage.

public class UserServiceTest extends BaseTest {

    @InjectMocks
    private UserService userService;

    // override the first return
    @Test
    public void testCheckEffectiveUser_0(a) {
        UserDto dto =new UserDto();
        boolean result = userService.checkEffectiveUser(dto);
        assertFalse(result);
    }

    // Override the second return
    @Test
    public void testCheckEffectiveUser_1(a) {
        UserDto dto =new UserDto();
        dto.setSex(1);
        boolean result = userService.checkEffectiveUser(dto);
        assertFalse(result);
    }

    // Override the third return
    @Test
    public void testCheckEffectiveUser_2(a) {
        UserDto dto =new UserDto();
        dto.setSex(1);
        dto.setFlag(1);
        boolean result = userService.checkEffectiveUser(dto);
        assertFalse(result);
    }

    // Override the fourth return
    @Test
    public void testCheckEffectiveUser_3(a) {
        UserDto dto =new UserDto();
        dto.setSex(1);
        dto.setFlag(1);
        dto.setAge(1);
        boolean result = userService.checkEffectiveUser(dto);
        assertFalse(result);
    }

    // Override the fifth return
    @Test
    public void testCheckEffectiveUser_4(a) {
        UserDto dto =new UserDto();
        dto.setSex(1);
        dto.setFlag(1);
        dto.setAge(18);
        booleanresult = userService.checkEffectiveUser(dto); assertTrue(result); }}Copy the code

Single test coverage detection detection

3. Verify method parameters using assertions

Assert: An assertion is a Reserved Java word used to debug a program, followed by a logical operation expression like this:

int a = 0, b = 1;
assert a == 0 && b == 0;
// Use javac to compile source file, and then java-EA class file name.
Copy the code

In Spring-Boot, you can use the spring-provided Assert class method to verify the parameters from the front end, such as:

// Check age >= 18 years old
public boolean checkUserAge(UserDto dto){
    Assert.notNull(dto.getAge(), "User age cannot be empty");
    Assert.isTrue(dto.getAge() >= 18."Users must be at least 18 years old.");
    return Boolean.TRUE;
}
Copy the code

If you need to convert to the uniform corresponding message returned by the REST API, you can do this by:

@ControllerAdvice
public class GlobalExceptionHandler {

    @ResponseBody
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Response<String> handleArgError(IllegalArgumentException e){
        return newResponse().failure().message(e.getMessage()); }}Copy the code

How to design a program

In the design process of functional modules, we should follow the following principles (refer to “Software Engineering – Structural Design Guidelines”) :

  1. Moderate module size
  2. Appropriate system call depth
  3. More fan in, less fan out (increase reuse, reduce dependence)
  4. Single entrance, single exit
  5. The scope of the module should be within the module
  6. The functionality should be predictable
  7. High cohesion, low coupling
  8. System decomposition has layers
  9. Less data redundancy

Reference documentation

  • testng.org/doc/
  • Github.com/powermock/p…
  • www.netconcepts.cn/detail-4100…