SpringBoot——Summary of unit testing practice

unit test

concept

In computer programming, unit testing (English: Unit Testing), also known as module testing, is a test for the correctness of program modules (the smallest unit of software design). A program unit is the smallest testable component of an application. In procedural programming, a unit is a single program, function, process, etc.; for object-oriented programming, the smallest unit is a method, including methods in base classes (superclasses), abstract classes, or derived classes (subclasses). --Wikipedia

effect

  1. Quality Improvement
    Single testing can reduce potential bugs to a certain extent and improve code quality. Unit testing not only solves the coverage problem, but also covers some boundary and exception handling problems in the code block.
  2. Refactoring
    The single test can escort other small partners to modify and refactor the code in the future, because as long as you dare to change the code randomly, the single test will dare to report an error for you.
  3. Debugging
    Unit testing is helpful for code debugging. We can mock dependent classes, methods, and parameters according to requirements, without real calls to downstream classes and methods.
  4. CodeReview
    single test is also a process of self-CodeReview. Separately testing the main flow, branches, boundaries, and exceptions of functional units helps to review whether the logical design of the code is reasonable. Reversely urge yourself to improve your awareness of coding quality. In particular, a method with hundreds of lines, when writing a single test, you will find this lump, why not dismantle it?

black and white box

black box testing

  Black box testing is also called 功能测试or 数据驱动测试, during the testing process, the program is regarded as a black box, and the internal code structure of the box cannot be seen.
  Black-box testing is mainly based on functional requirements to design test cases for testing, which is a testing method implemented from the outside of the software. Enter parameters multiple times to test to see if the program is normal or meets expectations.
  The black box only knows the function of the software (what it can do), but does not know the implementation of the software (how it works).

white box testing

  White-box testing is also called 结构测试or 逻辑驱动测试, during the testing process, the program is regarded as a transparent box, and the code and structure inside the box can be seen clearly, so that the tester has an understanding of the logic of the program code.
  Pass parameters in the way of exhaustive paths, and check whether all structures of the code are normal or meet expectations. Unit testing is white box testing.
  The white box knows the implementation of the software (how to do it), and does not need to care about the function of the software (what it can do).

logic coverage

1. Statement coverage

  Each executable statement of the program is executed at least once, that is, the test case covers all statements.

2. Judgment coverage

  Also known as branch coverage, for the decision expression, true or false two true and false judgments, each branch of the judgment in the program is experienced at least once.
  For example, the decision expression: a > 0 && b > 0
  design test data

a > 0 && b > 0(判定表达式的值为“真”)
a <= 0 && b <= 0(判定表达式的值为“假”)

  All branches that satisfy the decision are covered (at this time, both true and false branches are covered).

3. Condition coverage

  For the conditions in the judgment statement, each value of each condition in each judgment in the program is satisfied at least once, for the conditional statement.
  For example, the decision expression: a > 0 && b > 0
  design test data

a <= 0 && b <= 0(判定表达式的值为“假”)
a > 0 && b <= 0(判定表达式的值为“假”)

  Each condition is guaranteed to take a value once, regardless of whether the branch is fully covered (only false branches are covered at this time).

4. Condition/judgment coverage

  In the judgment condition, whether all possible conditions are true or not is executed at least once, and the possible results of all true and false judgments are executed at least once.
  For example, the decision expression: a > 0 && b > 0
  design test data

a > 0 && b > 0(判定表达式的值为“真”)
a <= 0 && b <= 0(判定表达式的值为“假”)

5. Condition combination coverage

  All possible combinations of conditional values ​​are executed at least once.
  For example, the decision expression: a > 0 && b > 0
  design test data

a > 0 && b > 0(判定表达式的值为“真”)
a <= 0 && b <= 0(判定表达式的值为“假”)
a > 0 && b <= 0(判定表达式的值为“假”)
a <= 0 && b > 0(判定表达式的值为“假”)

  All possible combinations of conditions in the decision.

6. Path coverage


  all possible execution paths.

Introduction to SpringBoot Engineering Single Test

pom dependencies

<dependency> 
<groupId>org.springframework.boot</groupId> 
<artifactId>spring-boot-starter-test</artifactId> 
<scope>test</scope> 
</dependency> 

Notice:

  1. The dependency version generally follows the corresponding SpringBoot version and does not need to be specified manually.
  2. This dependency will automatically introduce related dependencies:
    JUnit
    Mockito
    Spring Test
    … …

Idea structure

create path

  Create src/test at the same level as src/main, and keep other class paths consistent with the main package. (The path can be automatically created by creating a shortcut method of the class)

Create classes and methods

The shortcut method to create a class corresponding to the unit test class: just double-click the class, right-click the mouse, and select go to Test.

Select the method to be tested, at the same time, you can select the JUnit version, etc., click OK

Controller layer single test

code under test

@Slf4j
@RestController
@RequestMapping("/demo")
@Api(value = "DemoController", tags = "Demo管理模块")
public class DemoController implements DemoApi {
    
    
    /**
     * service
     */
    @Autowired
    private DemoService demoService;


    @Override
    @ApiOperation(value = "新增", notes = "新增")
    @PostMapping("/create")
    public Boolean add(@RequestHeader("appCode") String appCode,
                                     @RequestBody DemoDTO demoDTO) {
    
    
        boolean addFlag = demoService.add(demoDTO);
        if (addFlag) {
    
    
            // 刷新资源
          demoService.refreshMap(appCode);
        }
        return addFlag;
    }


    @Override
    @ApiOperation(value = "修改", notes = "修改")
    @PostMapping("/update")
    public Boolean update(@RequestHeader("appCode") String appCode,
                                        @RequestBody DemoDTO demoDTO) {
    
    
      
        boolean addFlag = demoService.update(demoDTO);
        if (addFlag) {
    
    
            // 刷新资源
          demoService.refreshMap(appCode);
        }
        return addFlag;
    }


    @Override
    @ApiOperation(value = "删除", notes = "删除")
    @DeleteMapping("/delById")
    public Boolean deleteById(@RequestHeader("appCode") String appCode, @RequestParam Long id) {
    
    
        boolean deleteFlag = demoService.deleteById(id);
        if (deleteFlag) {
    
    
            // 刷新资源
          demoService.refreshMap(appCode);
        }
        return addFlag;
    }


    @Override
    @ApiOperation(value = "列表", notes = "列表")
    @GetMapping("/list")
    public List<DemoVO> list() {
    
    
        return demoService.list();
    }
 }

test code

  1. Individual component testing, no need to use @SpringBootTest for overall context startup, use @RunWith(SpringRunner.class) to run test cases.
  2. Inject the controller class using @InjectMock.
  3. Annotate the mock service class with @Mock.
@RunWith(SpringRunner.class)
public class DemoControllerTest {
    
    
    /**
     * mock mvc
     */
    private MockMvc mockMvc;


    /**
     * 注入实例
     */
    @InjectMocks
    private DemoController demoController;


    /**
     * service mock
     */
    @Mock
    private DemoService demoService;
    
    /**
     * appCode
     */
    private String appCode;


    /**
     * before设置
     */
    @Before
    public void setUp() {
    
    
        //初始化带注解的对象
        MockitoAnnotations.openMocks(this);
        //构造mockmvc
        mockMvc = MockMvcBuilders.standaloneSetup(demoController).build();
        //appCode
        appCode = "AppCode_test";
    }


    /**
     * 测试testAdd
     */
    @Test
    public void testAdd() throws Exception {
    
    
        //构建dto
        DemoDTO demoDTO = new DemoDTO();
        //setId
        demoDTO.setId(-1L);
        //setName
        demoDTO.setName("test");
        //mock service方法
   		PowerMockito.when(demoService.add(demoDTO)).thenReturn(true);
        //构造body
        String body = JSONObject.toJSONString(demoDTO);
        //执行mockmvc
        this.mockMvc.perform(MockMvcRequestBuilders.post("/demo/create")
                //传参
                .header("appCode", appCode).content(body).contentType(MediaType.APPLICATION_JSON_VALUE))
                //mock返回
                .andExpect(status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn();
    }


    /**
     * 测试testUpdate
     */
    @Test
    public void testUpdate() throws Exception {
    
    
          //构建dto
        DemoDTO demoDTO = new DemoDTO();
        //setId
        demoDTO.setId(-1L);
        //setName
        demoDTO.setName("test");
        //mock service方法
        PowerMockito.when(demoService.update(demoDTO)).thenReturn(true);
        //构造body
        String body = JSONObject.toJSONString(demoDTO);
        //执行mockmvc
        this.mockMvc.perform(MockMvcRequestBuilders.post("/demo/update")
                //传参
                .header("appCode", appCode).content(body).contentType(MediaType.APPLICATION_JSON_VALUE))
                //mock返回
                .andExpect(status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn();
    }


    /**
     * 测试testDelete
     */
    @Test
    public void testDelete() throws Exception {
    
    
        //Id
        Long id = 1000L;
        //mock service方法
        PowerMockito.when(demoService.deleteById(id)).thenReturn(true);
        //执行mockmvc 方法一
//        this.mockMvc.perform(MockMvcRequestBuilders.delete("/demo/delById?id={id}",id)
//                //传参
//                .header("appCode", appCode).contentType(MediaType.APPLICATION_JSON_VALUE))
//                //mock返回
//.andExpect(status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn();
//    }
		//方法二
        this.mockMvc.perform(MockMvcRequestBuilders.delete("/demo/delById")
                //传参
                .header("appCode", appCode).param("id", "1000").contentType(MediaType.APPLICATION_JSON_VALUE))
                //mock返回
                .andExpect(status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn();
    }


    /**
     * 测试testList
     */
    @Test
    public void testList() throws Exception {
    
    
        this.mockMvc.perform(MockMvcRequestBuilders.get("/demo/list")
                //传参
                .contentType(MediaType.APPLICATION_JSON_VALUE))
                //mock返回
                .andExpect(status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn();
    }
}

Service layer single test

code under test

@Service 
public class MenuService {
    
     
    @Autowired 
    private MenuMapper menuMapper; 
    
    @Transactional(readOnly = true) 
    public List<Menu> listMenus() {
    
     
        final List<Menu> result = menuMapper.list(); 
        return result; 
    }
} 

test code

  1. By @TestConfiguration creating a test configuration, a declaration is provided in the configuration MenuService Bean.
  2. The use of this annotation has the following points: the annotated class must be static and cannot be private. It is recommended to be used in internal classes, otherwise the defined Bean will not be automatically loaded. Must be loaded in one of the following ways
@Import(MenuServiceTestConfig.class)
@ContextConfiguration(classes =MenuServiceTestConfig.class)
@Autowired
//此处通过 @Autowired 自动注入的是上面通过 @TestConfiguration 声明的 Bean。
@RunWith(SpringRunner.class) 
public class MenuServiceTest {
    
     
    @TestConfiguration 
    static class MenuServiceTestConfig {
    
     
        @Bean 
        public MenuService mockMenuService() {
    
     
            return new MenuService(); 
        } 
    }
    @Autowired 
    private MenuService MenuService; 
    @MockBean 
    private MenuMapper MenuMapper; 
    @Test 
    public void listMenus() {
    
     
        List<Menu> menus = new ArrayList<Menu>() {
    
    {
    
     
            this.add(new Menu()); 
        }}; 
        Mockito.when(menuMapper.list()).thenReturn(menus); 
        List<Menu> result = menuService.listMenus(); 
        Assertions.assertThat(result.size()).isEqualTo(menus.size()); 
    } 
}

single test protocol

  1. [Mandatory] A good unit test must follow the AIR principles.
    Explanation: When the unit test is running online, it feels like air (AIR) does not exist, but it is very critical to ensure the quality of the test. Macroscopically speaking, a good unit test has the characteristics of automation, independence, and repeatability.
AAutomatic(自动化)
IIndependent(独立性)
RRepeatable(可重复)
  1. [Mandatory] Unit tests should be executed fully automatically and non-interactively.
    Explanation: Test cases are usually executed on a regular basis, and the execution process must be fully automated to make sense. A test whose output requires human inspection is not a good unit test. System.out is not allowed to be used for human verification in unit tests, and assert must be used for verification.
  2. [Mandatory] Keep unit tests independent.
    Note: In order to ensure that the unit test is stable, reliable and easy to maintain, unit test cases must not call each other, nor can they rely on the order of execution.
    Counter-example: method2 needs to rely on the execution of method1, and uses the execution result as the input of method2.
  3. [Mandatory] Unit tests can be executed repeatedly and cannot be affected by the external environment.
    Description: Unit tests are usually placed in continuous integration, and unit tests will be executed every time there is code check in. If the unit test depends on the external environment (network, service, middleware, etc.), it will easily lead to the unavailability of the continuous integration mechanism.
    Positive example: In order not to be affected by the external environment, it is required to change the dependency of the SUT to injection when designing the code, and use a DI framework such as spring to inject a local (memory) implementation or Mock implementation during testing.
  4. [Mandatory] For unit tests, ensure that the test granularity is small enough to help pinpoint problems. Single measurement granularity is at most class level, usually method level.
    Note: Only when the test granularity is small can the error location be located as soon as possible when an error occurs. Unit tests are not responsible for checking cross-class or cross-system interaction logic, which is the domain of integration testing.
  5. [Mandatory] Incremental codes for core business, core applications, and core modules ensure that unit tests pass.
    Note: Add new code to supplement the unit test in time. If the new code affects the original unit test, please correct it in time.
  6. [Mandatory] The unit test code must be written in the following project directory: src/test/java, and it is not allowed to be written in the business code directory.
    Note: This directory will be skipped when the source code is built, and the unit test framework scans this directory by default.

PowerMock framework

mock introduction

mock concept

Mock refers to the creation of a virtual object to simulate the behavior of a specified object during testing.

mock effect

  1. Service-A depends on service-B. When testing service-A, service-B has not been developed yet. Some behaviors of service-B are simulated by mock to achieve the test effect.
  2. Objects that create private constructors cannot be constructed directly, but can be constructed through mocks.
  3. The tested module needs to connect to the database and other operations, and it cannot be guaranteed to be able to connect to the database during the test, which can be realized through mocking.
  4. … …

Introduction to PowerMock

  1. PowerMock is a Java single-test simulation framework that extends the EasyMock and Mockito frameworks.
  2. PowerMock performs mocking by providing custom classes as well as some bytecode manipulation tricks.
  3. PowerMock can simulate static methods, private methods, constructors, final methods, etc.
  4. PowerMock supports JUnit and TestNG.

PowerMock pom dependencies

<properties>
    <powermock.version>2.0.9</powermock.version>
</properties>
<dependencies>
   <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-api-mockito2</artifactId>
      <version>${
    
    powermock.version}</version>
      <scope>test</scope>
   </dependency>
</dependencies>

Notice:

This dependency is suitable for JUnit4.4 and above.

PowerMock function

  • mock constructors
  • mock final method or classes
  • mock private methods
  • mock static methods
  • Mock java system classes
    If you are interested, you can research on the official website. The version focuses on some commonly used mock functions.

Common mock examples of PowerMock

mock template

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
@PrepareForTest({
    
    XxxUtils.class})
public class DemoServiceImplTest {
    
    
    /**
     * 注入service
     */
    @InjectMocks
    private DemoServiceImpl demoService;

    /**
     * mapper
     */
    @MockBean
private DemoMapper demoMapper;

@Before
public void setUp() {
    
    
      //构建mybatis缓存
      TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), DemoEntity.class);
}

@Test
public void testCreateDemo() {
    
    
   //todo
}
}

mock private constructor

Take the Result class encapsulated by the department as an example, some of the private construction methods cannot construct parameters, and mock construction is required.

Result<List<UserInfoDTO>> result = Whitebox.invokeConstructor(Result.class, "200", "", true, userInfoDTOList);

Mock private methods and private properties

//mock 当前service类
ServiceA serviceMock= PowerMockito.mock(ServiceA.class);
//内部调用其他service需设置非公共成员
Whitebox.setInternalState(serviceMock, "serviceB", serviceB);
//mock调用其他service方法
PowerMockito.when(serviceB.getStr("xxx")).thenReturn("xxxxx");
//调用内部私有方法
Whitebox.invokeMethod(serviceMock, "privateMethod1", demoDto);

illustrate:

  1. The ServiceB class is injected into the ServiceA class, and the getStr(String str) method is called.
  2. The private method privateMethod1 in the ServiceA class is passed as a parameter to DemoDto.

mock static method

  1. Add static method annotations that need to be tested
@PrepareForTest({
    
    StaticXXX.class})
  1. Test mock call
//mock静态类
PowerMockito.mockStatic(StaticXXX.class);
//mock调用静态方法
PowerMockito.when(StaticXXX.method01("param01", "param02")).thenReturn(xxx);

mock configuration

@MockBean
private DemoProperties demoProperties ;
  1. Use @MockBean to introduce configuration classes
  2. call plug value
//构造demoProperties 
DemoProperties .DemoFirstProperties demoFirstProperties = new DemoProperties .DemoFirstProperties ();
//塞值
demoFirstProperties .setFirstParams("xxxx");
//mock properties
PowerMockito.when(demoProperties .getDemoFirstProperties ()).thenReturn(demoFirstProperties );

The public method value return of the mock object class

  1. object class
public class SmsResponse implements Serializable {
    
    
    /**
     * 编码 1-成功
     */
    private Integer code;
    /**
     * 返回内容
     */
    private String msg;
    public boolean isSuccess(){
    
    
        return null != code && 200 == code;
    }
    public Integer getCode() {
    
    
        return code;
    }
    public void setCode(Integer code) {
    
    
        this.code = code;
    }
    public String getMsg() {
    
    
        return msg;
    }
    public void setMsg(String msg) {
    
    
        this.msg = msg;
    }
}
  1. The return value of the isSuccess() method call in this class is used in the code
// 发送短信
SmsResponse smsResponse = this.smsNoticeService.sendSms(sms);
if (smsResponse.isSuccess()) {
    
    
    if (Objects.nonNull(noticeTimes) && Objects.nonNull(smsNotice)) {
    
    
        // 短信发送成功,更新通知标识
        noticeTimes.put(NoticeChannelEnum.SMS.getDesc(),smsNotice);
    }
    result.put(MSG,"");
    return result;
}

At this time, if you want to enter the if branch, you need to mock the operation

SmsResponse smsResponse = PowerMockito.mock(SmsResponse.class);
PowerMockito.when(smsResponse.isSuccess()).thenReturn(true);
when(this.smsNoticeService.sendSms(any(Sms.class))).thenReturn(smsResponse);

demining

@SpringBootTest

question:

  1. @SpringBootTest will scan the Spring configuration of the application and build a complete Spring Context. Every time a unit test of a class is executed, the entire context needs to be started, and the single test speed is slow!
  2. When @SpringBootTest loads the Spring context, it may cause the test class to start due to the failure to load the database, MQ, cache and other configurations used in the service.
  3. @SpringBootTest is more suitable for functional integration testing.

solution:

Use @RunWith(SpringRunner.class) to declare that the test is performed in the Spring environment, so that Spring-related annotations will be recognized and effective.

Combined use of SpringBoot+PowerMock+Mockito

question:

When used in combination with powermock, it is easy to cause conflicts between some classes and methods, resulting in the method not being found.

solution:

Dependency version corresponds to:

Mockito PowerMock
2.8.9+ 2.x
2.8.0-2.8.9 1.7.x
2.7.5 1.7.0RC4
2.4.0 1.7.0RC2
2.0.0-beta - 2.0.42-beta 1.6.5-1.7.0RC
1.10.8 - 1.10.x 1.6.2 - 2.0
1.9.5-rc1 - 1.9.5 1.5.0 - 1.5.6
1.9.0-rc1 & 1.9.0 1.4.10 - 1.4.12
1.8.5 1.3.9 - 1.4.9
1.8.4 1.3.7 & 1.3.8
1.8.3 1.3.6
1.8.1 & 1.8.2 1.3.5
1.8 1.3
1.7 1.2.5

constructor injection class

question:

Some other classes injected in some service classes are injected through constructors instead of @Autowired or @Resource annotations. like

@Service 
public class MenuService {
    
     
   
    private MenuMapper menuMapper; 
@Autowired 
public MenuService(final MenuMapper menuMapper) {
    
    
    this.menuMapper = menuMapper;
}

    @Transactional(readOnly = true) 
    public List<Menu> listMenus() {
    
     
        final List<Menu> result = menuMapper.list(); 
        return result; 
    }
} 

solution:

Construct service

@RunWith(SpringRunner.class) 
public class MenuServiceTest {
    
     

private static final MenuMapper menuMapper = Mockito.mock(MenuMapper.class);

    @TestConfiguration 
    static class MenuServiceTestConfig {
    
     
        @Bean 
        public MenuService mockMenuService() {
    
     
            return new MenuService(menuMapper); 
        } 
    }
    @Autowired 
    private MenuService MenuService; 
    @MockBean 
    private MenuMapper MenuMapper; 
    @Test 
    public void listMenus() {
    
     
        List<Menu> menus = new ArrayList<Menu>() {
    
    {
    
     
            this.add(new Menu()); 
        }}; 
        Mockito.when(menuMapper.list()).thenReturn(menus); 
        List<Menu> result = menuService.listMenus(); 
        Assertions.assertThat(result.size()).isEqualTo(menus.size()); 
    } 
}

Static method mock reports error org.powermock.api.mockito.ClassNotPreparedExceptionnot

question:

When mocking a static method, an error is reported that the static class is prepared for test

solution:

  1. Confirm whether the @PrepareForTest({Xxx.class}) annotation is added to the class to prepare the static class.
  2. Confirm whether the @RunWith(PowerMockRunner.class) runtime environment is used when using PowerMock.

Mybatis reports MybatisPlusException

question:

Where the lambda expression has a set method, the single test reports an errorcom.baomidou.mybatisplus.core.exceptions.MybatisPlusException: can not find lambda cache for this entity

solution:

Add code to the @Before method in the unit test class to manually trigger the collection of cache information.

@Before
public void setUp() {
    
    
    //构建mybatis缓存
    TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), XxxEntity.class);
}

Among them, XxxEntity.class is the table entity class.

reference

  1. single test protocol
  2. junit official website
  3. spring-boot-testing
  4. mybatis-plus code base

Guess you like

Origin blog.csdn.net/Andya_net/article/details/129264354