热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

一聊起测试用例,很多人第一反应就是,我们公司的测试会写测试用例的,我自己也会使用postman或者swagger之类的进行代码自测。那我们研发到底要不要写单元测试用例呢?参考阿里巴

01、为什么要写单元测试

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

  1. 保证代码质量!!!无论初级,中级,高级攻城狮开发工程的代码,且不说效率如何,功能是必要要保证是正确的;交付测试以后,bug锐减,联调飞快。

  2. 代码逻辑“文档化”!!!新人接手维护模块代码时,通过单元测试用例,以debug的方式就能熟悉业务代码。比起,看代码,研究表结构梳理代码结构,效率提升飞快。

  3. 易维护!!!新人接手维护代码模块时,提交自己的代码时,远行之前的单元测试达到回归测试,保证了新改动不会影响老业务。

  4. 快速定位bug!!!在联调期间,测试提出bug后,基于uat环境,编写出错的api测试用例。根据,测试提供的参数和token就可以以debug的方式跟踪问题的所在,如果是在微服务架构中,运行单元测试用例,不会注册本地服务到uat环境,还能过正常请求注册中心的服务。

02、到底如何写单元测试

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

基于MockitoJUnitRunner的方法测试

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

@Service public class MenuManagerImpl implements IMenuManager {     /**      * 删除菜单业务逻辑      **/     @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("删除菜单失败!");         }         return true;     } }  /**   * 删除菜单方法级单元测试用例   **/ @RunWith(MockitoJUnitRunner.class) public class MenuManagerImplTest {     @InjectMocks     private MenuManagerImpl menuManager;     @Mock     private IMenuService menuService;     @Test     public void delete() {         Long id = null;         boolean flag;         // id为空         flag = menuManager.delete(id);         Assert.assertFalse(flag);         // 菜单返回为空         id = 1l;         Mockito.when(this.menuService.getById(ArgumentMatchers.anyLong())).thenReturn(null);         flag = menuManager.delete(id);         Assert.assertFalse(flag);         // 修改成功         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);     } }

基于SpringRunner的Spring容器测试

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

/**  * 成功新增菜单api */ @Api(tags = "管理员菜单api") @RestController public class AdminMenuController {     @Autowired     private IMenuManager menuManager;     @PreAuthorize("hasAnyAuthority('menu:add','admin')")     @ApiOperation(value = "新增菜单")     @PostMapping("/admin/menu/add")     @VerifyLoginUser(type = IS_ADMIN, errorMsg = INVALID_ADMIN_TYPE)     public Response save(@Validated @RequestBody SaveMenuDto saveMenuDto) {         return Response.success(menuManager.save(saveMenuDto));     } } /**  * 成功新增菜单单元测试用例 */ @RunWith(SpringRunner.class) @SpringBootTest(classes = MallSystemApplication.class) @Slf4j @AutoConfigureMockMvc public class AdminMenuControllerTest extends BaseTest { /**  * 成功新增菜单 */ @Test public void success2save() throws Exception {         SaveMenuDto saveMenuDto = new SaveMenuDto();         saveMenuDto.setName("重置密码");         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");         // 发起http请求         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 response = JSON.parseObject(mvcResult.getResponse().getContentAsString(), menuVoTypeReference);         // 断言结果         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());     } }

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

  1. 业务参数的校验,和义务异常的校验。例如,名称是否为空,电话号码是否正确,用户未登陆则抛出未登陆异常。

  2. 各类业务场景的真实测试用例,例如,编写成功添加顶级菜单的测试用例,已经编写成功添加子级菜单的测试用例。

注意事项

  • 配置覆盖

此外,如上基于mockmvc的编写的测试用例,由于加载了Spring的配置,会对项目发起真实的调用。如果,环境的配置为线上配置,容易出现安全问题;一般,处于安全考虑,很多公司会对真实环境的修改操作做事务回滚操作,甚至根本就不会进行真实环境的调用,使用模拟环境替换,例如数据库的操作可以使用h2内存数据库进行替换。

这时,可以在src/test/resources目录下,添加与src/main/resources目录下,相同的文件进行配置覆盖。src/test/main目录下的代码,会首先加载src/test/resources目录下的配置,如果没有则在加载src/main/resources目录的配置。常用场景如下:

  1. 在单元测试环境使用使用内存数据库。

  2. ginkens代码集成运行测试用例时,不希望在集成环境中输出日志文件信息,并且以debug级别输出日志。

以日志文件配置覆盖为例,在src/main/resources目录下配置日志有文件和控制台输出,如图:

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

 mall-system                   [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level][%thread] [%contextName] [%logger{80}:%L] %msg%n        UTF-8          DEBUG               log/info.log       log/info-%d{yyyy-MM-dd}.%i.log    50MB    50    10GB          [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level] [%contextName] [%logger{80}:%L] %msg%n                    

src/test//resource目录下的新增logback-spring.xml,去掉日志文件输出的配置,设置日志输出级别为DEBUG;如果运行测试用例,则加载该配置不会进行日志文件的输出,并且打印DEBUG级别日志。如图:

    mall-system                                                      [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level][%thread] [%contextName] [%logger{80}:%L] %msg%n                          UTF-8                               DEBUG                                      

  • 指定环境

一般开发过程中,我们研发只会操作开发环境,也是为了避免数据安全问题,可以在单元测试用例中指定运行的环境配置。在测试类加上@ActiveProfiles("dev"),指定获取dev环境的配置。示例,

/** * 获取dev环境配置 */ @RunWith(SpringRunner.class) @SpringBootTest(classes = MallSystemApplication.class) @Slf4j @AutoConfigureMockMvc @ActiveProfiles("dev") public class AdminMenuControllerTest extends BaseTest { }

在联调测试中,对于出错的api,可以编写对应的单元测试用例,使用@ActiveProfiles("uat")指定到测试环境,就可以根据测试提供的参数快速定位问题。示例:

/**  * 新增菜单api联调 */ @RunWith(SpringRunner.class) @SpringBootTest(classes = MallSystemApplication.class) @Slf4j @AutoConfigureMockMvc @ActiveProfiles("uat") public class AdminMenuControllerTest extends BaseTest { /**  * 成功新增菜单 */ @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"    }";    // 发起http请求    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();     } }


绵薄之力

最后感谢每一个认真阅读我文章的人,看着粉丝一路的上涨和关注,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走

​这些资料,对于在从事【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!凡事要趁早,特别是技术行业,一定要提升技术功底。希望对大家有所帮助…….


推荐阅读
author-avatar
zhanghuabing
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有