Why write unit tests
When talking about test cases, many people's first reaction is that our company's tests will write test cases, and I will also use postman or swagger for code self-test. Should we write unit test cases?
Referring to Alibaba's development manual, rule 8 (the basic goal of unit testing: the statement coverage rate should reach 70%; the statement coverage rate and branch coverage rate of the core module should reach 100%), which is required by large manufacturers. I personally feel that it is necessary to write unit test cases, which have many advantages, such as:
-
Ensure code quality!!! Regardless of the primary, intermediate and advanced siege lion development project code, regardless of the efficiency, the function is necessary to ensure that it is correct; After the delivery test, the number of bug s decreased sharply and the joint commissioning was fast.
-
Code logic "documented"!!! When newcomers take over the maintenance of module code, they can be familiar with the business code by means of debug ging through unit test cases. Compared with looking at the code, studying the table structure and combing the code structure, the efficiency is improved rapidly.
-
Easy to maintain!!! When the newcomers take over the maintenance of the code module and submit their own code, the unit test before the trip reaches the regression test, which ensures that the new changes will not affect the old business.
-
Quickly locate bugs!!! During the joint commissioning, after the test puts forward a bug, write the wrong api test case based on the uat environment. According to the, the parameters and token s provided by the test can track the problem in the way of debug ging. If the unit test case is run in the micro service architecture, the local service will not be registered to the uat environment, and the service of the registration center can be requested normally.
How to write unit tests
Java development springboot projects are based on junit test framework. Compare the use of MockitoJUnitRunner with Spring runner. MockitoJUnitRunner is based on mockito to simulate business conditions and verify code logic. SpringRunner is a subclass of MockitoJUnitRunner, which integrates the Spring container and can load Spring bean objects according to the configuration in the test.
In the development of Springboot, the project configuration is loaded and unit tested in combination with the @ SpringBootTest annotation.
Method testing based on mockitojunit runner
Take the springboot project as an example. Generally, mock tests are performed on individual methods. In the test method, mockitojunit runner is used to cover the tests according to different conditions. Using @ InjectMocks annotation, the simulated method can normally initiate requests@ Mock annotations can simulate the desired conditions. Take deleting the menu service as an example. The source code is as follows:
@Service public class MenuManagerImpl implements IMenuManager { /** * Delete menu business logic **/ @Override @OptimisticRetry @Transactional(rollbackFor = Exception.class) public boolean delete(Long id) { if (Objects.isNull(id)) { return false; } Menu existingMenu = this.menuService.getById(id); if (Objects.isNull(existingMenu)) { return false; } if (!this.menuService.removeById(id)) { throw new OptimisticLockingFailureException("Failed to delete menu!"); } return true; } } /** * Delete menu method level unit test cases **/ @RunWith(MockitoJUnitRunner.class) public class MenuManagerImplTest { @InjectMocks private MenuManagerImpl menuManager; @Mock private IMenuService menuService; @Test public void delete() { Long id = null; boolean flag; // id is empty flag = menuManager.delete(id); Assert.assertFalse(flag); // The menu returns empty id = 1l; Mockito.when(this.menuService.getById(ArgumentMatchers.anyLong())).thenReturn(null); flag = menuManager.delete(id); Assert.assertFalse(flag); // Modified successfully Menu mockMenu = new Menu(); Mockito.when(this.menuService.getById(ArgumentMatchers.anyLong())).thenReturn(mockMenu); Mockito.when(this.menuService.removeById(ArgumentMatchers.anyLong())).thenReturn(true); flag = menuManager.delete(id); Assert.assertTrue(flag); } }
Spring container testing based on spring runner
In the process of api development, the call link of a single api will be verified, the third-party service will be mock simulated, and the business logic of the service will be tested.
Generally, @ SpringBootTest will be used to load the Spring container configuration of the test environment, and MockMvc will be used to test in the way of http request. Take the modified and added menu test case as an example, as follows:
/** * Successfully added menu api */ @Api(tags = "Administrator menu api") @RestController public class AdminMenuController { @Autowired private IMenuManager menuManager; @PreAuthorize("hasAnyAuthority('menu:add','admin')") @ApiOperation(value = "Add menu") @PostMapping("/admin/menu/add") @VerifyLoginUser(type = IS_ADMIN, errorMsg = INVALID_ADMIN_TYPE) public Response<MenuVo> save(@Validated @RequestBody SaveMenuDto saveMenuDto) { return Response.success(menuManager.save(saveMenuDto)); } } /** * Successfully added menu unit test case */ @RunWith(SpringRunner.class) @SpringBootTest(classes = MallSystemApplication.class) @Slf4j @AutoConfigureMockMvc public class AdminMenuControllerTest extends BaseTest { /** * Successfully added menu */ @Test public void success2save() throws Exception { SaveMenuDto saveMenuDto = new SaveMenuDto(); saveMenuDto.setName("reset password "); saveMenuDto.setParentId(1355339254819966978l); saveMenuDto.setOrderNum(4); saveMenuDto.setType(MenuType.button.getValue()); saveMenuDto.setVisible(MenuVisible.show.getValue()); saveMenuDto.setUrl("https:baidu.com"); saveMenuDto.setMethod(MenuMethod.put.getValue()); saveMenuDto.setPerms("user:reset-pwd"); // Initiate http request MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders .post("/admin/menu/add") .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE) .content(JSON.toJSONString(saveMenuDto)) .accept(MediaType.APPLICATION_JSON_UTF8_VALUE) .header(GlobalConstant.AUTHORIZATION_HEADER, GlobalConstant.ADMIN_TOKEN)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()) .andReturn(); Response<MenuVo> response = JSON.parseObject(mvcResult.getResponse().getContentAsString(), menuVoTypeReference); // assertion results Assert.assertNotNull(response); MenuVo menuVo; Assert.assertNotNull(menuVo = response.getData()); Assert.assertEquals(menuVo.getName(), saveMenuDto.getName()); Assert.assertEquals(menuVo.getOrderNum(), saveMenuDto.getOrderNum()); Assert.assertEquals(menuVo.getType(), saveMenuDto.getType()); Assert.assertEquals(menuVo.getVisible(), saveMenuDto.getVisible()); Assert.assertEquals(menuVo.getStatus(), MenuStatus.normal.getValue()); Assert.assertEquals(menuVo.getUrl(), saveMenuDto.getUrl()); Assert.assertEquals(menuVo.getPerms(), saveMenuDto.getPerms()); Assert.assertEquals(menuVo.getMethod(), saveMenuDto.getMethod()); } }
The specific rules for writing unit test cases refer to the writing of test cases. In short, two types of unit test cases for general APIs are written as follows:
-
Verification and sum obligations of business parameters Verification of exceptions . For example, whether the name is empty, whether the phone number is correct, and if the user does not log in, an unlisted exception will be thrown.
-
Real test cases for various business scenarios, for example, test cases for successfully adding top-level menus have been written, and test cases for successfully adding sub level menus have been written.
matters needing attention
Configure override
In addition, the above test cases written based on mockmvc will initiate real calls to the project because the Spring configuration is loaded. If the environment is configured online, it is prone to security problems.
Generally, for security reasons, many companies will roll back the transaction for the modification of the real environment, or even do not call the real environment at all. For example, the database operation can be replaced by h2 memory database.
At this time, you can add the same file in src/test/resources directory as in src/main/resources directory to overwrite the configuration. The code in the src/test/main directory will first load the configuration in the src/test/resources directory. If not, the configuration in the src/main/resources directory will be loaded. Common scenarios are as follows:
-
Use in unit test environment Memory database.
-
When ginkens code integration runs test cases, it does not want to output log file information in the integration environment, and the log is output at the debug level.
Taking the log file configuration overwrite as an example, configure the log file and console output in the src/main/resources directory, as shown in the figure:
logback-spring.xml in the main/resource directory, as follows:
<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true"> <contextName>mall-system</contextName> <!-- Console log output configuration --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern> [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level][%thread] [%contextName] [%logger{80}:%L] %msg%n </pattern> <charset>UTF-8</charset> </encoder> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>DEBUG</level> </filter> </appender> <!-- Log file output configuration --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>log/info.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>log/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxFileSize>50MB</maxFileSize> <maxHistory>50</maxHistory> <totalSizeCap>10GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level] [%contextName] [%logger{80}:%L] %msg%n</pattern> </encoder> </appender> <!-- set up INFO Level output log --> <root level="INFO"> <appender-ref ref="STDOUT" /> <appender-ref ref="FILE" /> </root> </configuration>
Add logback-spring.xml in src/test//resource directory, remove the configuration of log file output, and set Log output level Is DEBUG; If the test case is run, loading the configuration will not output the log file, and the DEBUG level log will be printed. As shown in the figure:
<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true"> <contextName>mall-system</contextName> <!-- Console log output --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern> [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level][%thread] [%contextName] [%logger{80}:%L] %msg%n </pattern> <charset>UTF-8</charset> </encoder> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>DEBUG</level> </filter> </appender> <!-- DEBUG Level log output --> <root level="DEBUG"> <appender-ref ref="STDOUT"/> </root> </configuration>
Specify environment
In the general development process, we only operate the development environment. In order to avoid data security problems, we can specify the running environment configuration in the unit test case. Add @ ActiveProfiles("dev") to the test class to specify the configuration of the dev environment. Example,
/** * Get dev environment configuration */ @RunWith(SpringRunner.class) @SpringBootTest(classes = MallSystemApplication.class) @Slf4j @AutoConfigureMockMvc @ActiveProfiles("dev") public class AdminMenuControllerTest extends BaseTest { }
In the joint commissioning test, for the api with errors, you can write corresponding unit test cases and specify them to the test environment using @ ActiveProfiles("uat"), so that you can quickly locate the problems according to the parameters provided by the test. Example:
/** * Add menu api joint debugging */ @RunWith(SpringRunner.class) @SpringBootTest(classes = MallSystemApplication.class) @Slf4j @AutoConfigureMockMvc @ActiveProfiles("uat") public class AdminMenuControllerTest extends BaseTest { /** * Successfully added menu */ @Test public void success2save() throws Exception { String token="Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjhjMjhlZWEzLTA5MWEtNDA1OS1iMzliLTRjOGMyNGY4ZjEzMiJ9.xK9srWjeGaq4NXt4BzG2MQ_yN9IaYtPVjKj5MoSS4bX9Ytf1XJNe_NSupR0IItkB48G6mXVZwj5CIwWIYzvsEA"; String paramJson="{ "name":"mayuan", "parentId":"1", "orderNum":"1", "type":"1", "visible":true, "url":"https:baidu.com", "method":2, "perms":"user:reset-pwd" }"; // Initiate http request MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders .post("/admin/menu/add") .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE) .content(paramJson) .accept(MediaType.APPLICATION_JSON_UTF8_VALUE) .header(GlobalConstant.AUTHORIZATION_HEADER, token)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()) .andReturn(); } }