飞道的博客

为什么要写单元测试?基于junit如何写单元测试?

398人阅读  评论(0)

为什么要写单元测试

一聊起测试用例,很多人第一反应就是,我们公司的测试会写测试用例的,我自己也会使用postman或者swagger之类的进行代码自测。那我们研发到底要不要写单元测试用例呢?参考阿里巴巴开发手册,第8条规则(单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率都要达到 100%),大厂的要求就是必须喽。我个人感觉,写单元测试用例也是很有必要的,好处很多,例如:

  1. 保证代码质量!!!无论初级,中级,高级开发工程的代码,功能是必要要保证是正确的;交付测试以后,bug锐减,联调飞快。
  2. 代码逻辑“文档化”!!!新人接手维护模块代码时,通过单元测试用例,以debug的方式就能熟悉业务代码。比起,看代码,研究表结构梳理代码结构,效率提升飞快。
  3. 易维护!!!新人接手维护代码模块时,提交自己的代码时,远行之前的单元测试达到回归测试,保证了新改动不会影响老业务。

到底如何写单元测试

Java开发springboot项目都是基于junit测试框架,比较MockitoJUnitRunner与SpringRunner与使用,MockitoJUnitRunner基于mockito,模拟业务条件,验证代码逻辑。SpringRunner是MockitoJUnitRunner子类,集成了Spring容器,可以在测试的根据配置加载Spring bean对象。在Springboot开发中,结合@SpringBootTest注解,加载项目配置,进行单元测试。

基于MockitoJUnitRunner的方法测试

以springboot项目为例,一般,对单个的方法都是进行mock测试,在测试方法使用MockitoJUnitRunner,根据不同条件覆盖测试。使用@InjectMocks注解,可以让模拟的方法正常发起请求;@Mock注解可以模拟期望的条件。以删除菜单服务为例,源码如下:


  
  1. @Service
  2. public class MenuManagerImpl implements IMenuManager {
  3. /**
  4. * 删除菜单业务逻辑
  5. **/
  6. @Override
  7. @OptimisticRetry
  8. @Transactional(rollbackFor = Exception.class)
  9. public boolean delete(Long id) {
  10. if (Objects.isNull(id)) {
  11. return false;
  12. }
  13. Menu existingMenu = this.menuService.getById(id);
  14. if (Objects.isNull(existingMenu)) {
  15. return false;
  16. }
  17. if (! this.menuService.removeById(id)) {
  18. throw new OptimisticLockingFailureException( "删除菜单失败!");
  19. }
  20. return true;
  21. }
  22. }
  23. /**
  24. * 删除菜单方法级单元测试用例
  25. **/
  26. @RunWith(MockitoJUnitRunner.class)
  27. public class MenuManagerImplTest {
  28. @InjectMocks
  29. private MenuManagerImpl menuManager;
  30. @Mock
  31. private IMenuService menuService;
  32. @Test
  33. public void delete() {
  34. Long id = null;
  35. boolean flag;
  36. // id为空
  37. flag = menuManager.delete(id);
  38. Assert.assertFalse(flag);
  39. // 菜单返回为空
  40. id = 1l;
  41. Mockito.when( this.menuService.getById(ArgumentMatchers.anyLong())).thenReturn( null);
  42. flag = menuManager.delete(id);
  43. Assert.assertFalse(flag);
  44. // 修改成功
  45. Menu mockMenu = new Menu();
  46. Mockito.when( this.menuService.getById(ArgumentMatchers.anyLong())).thenReturn(mockMenu);
  47. Mockito.when( this.menuService.removeById(ArgumentMatchers.anyLong())).thenReturn( true);
  48. flag = menuManager.delete(id);
  49. Assert.assertTrue(flag);
  50. }
  51. }

基于SpringRunner的Spring容器测试

在api开发过程中,会对单个api的调用链路进行验证,对第三方服务进行mock模拟,本服务的业务逻辑进行测试。一般,会使用@SpringBootTest加载测试环境的Spring容器配置,使用MockMvc以http请求的方式进行测试。以修改新增菜单测试用例为例,如下:


  
  1. /**
  2. * 成功新增菜单api
  3. */
  4. @Api(tags = "管理员菜单api")
  5. @RestController
  6. public class AdminMenuController {
  7. @Autowired
  8. private IMenuManager menuManager;
  9. @PreAuthorize("hasAnyAuthority('menu:add','admin')")
  10. @ApiOperation(value = "新增菜单")
  11. @PostMapping("/admin/menu/add")
  12. @VerifyLoginUser(type = IS_ADMIN, errorMsg = INVALID_ADMIN_TYPE)
  13. public Response<MenuVo> save(@Validated @RequestBody SaveMenuDto saveMenuDto) {
  14. return Response.success(menuManager.save(saveMenuDto));
  15. }
  16. }
  17. /**
  18. * 成功新增菜单单元测试用例
  19. */
  20. @RunWith(SpringRunner.class)
  21. @SpringBootTest(classes = MallSystemApplication.class)
  22. @Slf4j
  23. @AutoConfigureMockMvc
  24. public class AdminMenuControllerTest extends BaseTest {
  25. /**
  26. * 成功新增菜单
  27. */
  28. @Test
  29. public void success2save() throws Exception {
  30. SaveMenuDto saveMenuDto = new SaveMenuDto();
  31. saveMenuDto.setName( "重置密码");
  32. saveMenuDto.setParentId( 1355339254819966978l);
  33. saveMenuDto.setOrderNum( 4);
  34. saveMenuDto.setType(MenuType.button.getValue());
  35. saveMenuDto.setVisible(MenuVisible.show.getValue());
  36. saveMenuDto.setUrl( "https:baidu.com");
  37. saveMenuDto.setMethod(MenuMethod.put.getValue());
  38. saveMenuDto.setPerms( "user:reset-pwd");
  39. // 发起http请求
  40. MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders
  41. .post( "/admin/menu/add")
  42. .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
  43. .content(JSON.toJSONString(saveMenuDto))
  44. .accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
  45. .header(GlobalConstant.AUTHORIZATION_HEADER, GlobalConstant.ADMIN_TOKEN))
  46. .andExpect(MockMvcResultMatchers.status().isOk())
  47. .andDo(MockMvcResultHandlers.print())
  48. .andReturn();
  49. Response<MenuVo> response = JSON.parseObject(mvcResult.getResponse().getContentAsString(), menuVoTypeReference);
  50. // 断言结果
  51. Assert.assertNotNull(response);
  52. MenuVo menuVo;
  53. Assert.assertNotNull(menuVo = response.getData());
  54. Assert.assertEquals(menuVo.getName(), saveMenuDto.getName());
  55. Assert.assertEquals(menuVo.getOrderNum(), saveMenuDto.getOrderNum());
  56. Assert.assertEquals(menuVo.getType(), saveMenuDto.getType());
  57. Assert.assertEquals(menuVo.getVisible(), saveMenuDto.getVisible());
  58. Assert.assertEquals(menuVo.getStatus(), MenuStatus.normal.getValue());
  59. Assert.assertEquals(menuVo.getUrl(), saveMenuDto.getUrl());
  60. Assert.assertEquals(menuVo.getPerms(), saveMenuDto.getPerms());
  61. Assert.assertEquals(menuVo.getMethod(), saveMenuDto.getMethod());
  62. }
  63. }

具体编写单元测试用例规则参考测试用例的编写。简单说,一般api的单元测试用例,编写两类,如下:

  1. 业务参数的校验,和义务异常的校验。例如,名称是否为空,电话号码是否正确,用户未登陆则抛出未登陆异常。
  2. 各类业务场景的真实测试用例,例如,编写成功添加顶级菜单的测试用例,已经编写成功添加子级菜单的测试用例。

注意事项

此外,如上的编写的测试用例,会对项目发起真实的调用,如果,环境的配置为线上配置,容易出现安全问题。一般,处于安全考虑,很多公司不会进行真实环境的调用。这时,可以在src/test/resources目录下,添加与src/main/resources目录下,相同的文件进行配置覆盖。src/test/main目录下的代码,会首先加载src/test/resources目录下的配置,如果没有则在加载src/main/resources目录的配置。示例,在单元测试环境使用内存数据库(h2数据库),在单元测试环境不输出日志文件等等。以日志文件为例,如图:

main/resource目录下的logback-spring.xml,内容如下:


  
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration scan="true">
  3. <contextName>mall-system </contextName>
  4. <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  5. <encoder>
  6. <pattern>
  7. [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level][%thread] [%contextName] [%logger{80}:%L] %msg%n
  8. </pattern>
  9. <charset>UTF-8 </charset>
  10. </encoder>
  11. <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
  12. <level>DEBUG </level>
  13. </filter>
  14. </appender>
  15. <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
  16. <file>log/info.log </file>
  17. <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
  18. <fileNamePattern>log/info-%d{yyyy-MM-dd}.%i.log </fileNamePattern>
  19. <maxFileSize>50MB </maxFileSize>
  20. <maxHistory>50 </maxHistory>
  21. <totalSizeCap>10GB </totalSizeCap>
  22. </rollingPolicy>
  23. <encoder>
  24. <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level] [%contextName] [%logger{80}:%L] %msg%n </pattern>
  25. </encoder>
  26. </appender>
  27. <root level="DEBUG">
  28. <appender-ref ref="STDOUT" />
  29. <appender-ref ref="FILE" />
  30. </root>
  31. <logger name="com.kuqi.mall.system.dao" level="DEBUG" />
  32. </configuration>

 src/test//resource目录下的logback-spring.xml,屏蔽了file文件日志输出,只有控制台日志输出,内容如下:


  
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration scan="true">
  3. <contextName>mall-system </contextName>
  4. <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  5. <encoder>
  6. <pattern>
  7. [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level][%thread] [%contextName] [%logger{80}:%L] %msg%n
  8. </pattern>
  9. <charset>UTF-8 </charset>
  10. </encoder>
  11. <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
  12. <level>DEBUG </level>
  13. </filter>
  14. </appender>
  15. <root level="DEBUG">
  16. <appender-ref ref="STDOUT"/>
  17. </root>
  18. <logger name="com.kuqi.mall.system.dao" level="DEBUG"/>
  19. </configuration>

 


转载:https://blog.csdn.net/new_com/article/details/116098959
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场