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 {@InjectMocksprivate MenuManagerImpl menuManager;@Mockprivate IMenuService menuService;@Testpublic 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 {@Autowiredprivate IMenuManager menuManager;@PreAuthorize("hasAnyAuthority('menu:add','admin')")@ApiOperation(value = "新增菜单")@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));}
}/*** 成功新增菜单单元测试用例
*/
@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<MenuVo> 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,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true"><contextName>mall-system</contextName><!-- 控制台日志输出配置 --><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><!-- 日志文件输出配置 --><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><!-- 设置INFO 级别输出日志 --><root level="INFO"><appender-ref ref="STDOUT" /><appender-ref ref="FILE" /></root>
</configuration>

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

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true"><contextName>mall-system</contextName><!-- 控制台日志输出 --><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级别日志输出 --><root level="DEBUG"><appender-ref ref="STDOUT"/></root>
</configuration>
  • 指定环境

一般开发过程中,我们研发只会操作开发环境,也是为了避免数据安全问题,可以在单元测试用例中指定运行的环境配置。在测试类加上@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();}
}

绵薄之力

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

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

为什么要写单元测试?如何写单元测试?相关推荐

  1. 文件上传的单元测试怎么写?

    早上有个群友问了一个不错的问题:文件上传的单元测试怎么写?后面也针对后端开发要不要学一下单元测试的话题聊了聊,个人是非常建议后端开发能够学一下单元测试的.所以,今天特地拿出来写一篇说说,并不是因为这有 ...

  2. 如何写出优秀的单元测试

    单元测试已是软件工程师必备的技能,但在我的经验中,有些人写的单元测试实际上却没测到重点,而且还容易因为重构而导致测试失败,可说是为了测试而测试.这样的测试不仅不会带来好处,反而还使专项更不稳健,因此遵 ...

  3. 单元测试,写起来到底有多痛?你会了吗

    到底什么是单元测试 这个问题看似非常简单,单元测试嘛,不就是咱们开发自己写些测试类,来测试自己写的代码逻辑对不对. 这句话没有问题,但是不够准确. 首先我们要明白,这个测试二字前面还有两个字:单元. ...

  4. 该死的单元测试,写起来到底有多痛?

    今天带大家看看单元测试到底该怎么写. 到底什么是单元测试 这个问题看似非常简单,单元测试嘛,不就是咱们开发自己写些测试类,来测试自己写的代码逻辑对不对. 这句话没有问题,但是不够准确. 首先我们要明白 ...

  5. python单元测试怎么写_python--单元测试

    1. 单元测试是什么? 单元测试(又称为模块测试, Unit Testing)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作.程序单元是应用的最小可测试部件.在过程化编程中,一个单元就 ...

  6. Python单元测试介绍及单元测试理解,单元测试的自动生成(对函数进行测试)

    目录 一.单元测试的定义 二.实例理解 2.1可通过的测试 一个模拟的登录 测试用例 测试代码 运行结果 2.2不可通过的测试 一个模拟的登录 测试用例 测试代码 运行结果 三.单元测试的自动生成 h ...

  7. springboot项目编写单元测试_SpringBoot项目单元测试(示例代码)

    前一段时间,有朋友问到springboot运用如何进行单元测试,结合LZ公司的实际运用,这里给大家描述一下三种单元测试的方式. 1.约定 单元测试代码写在src/test/java目录下 单元测试类命 ...

  8. 写代码犹如写文章: “大师级程序员把系统当故事来讲,而不是当做程序来写” | 如何架构设计复杂业务系统? 如何写复杂业务代码?

    写代码犹如写文章: "大师级程序员把系统当故事来讲,而不是当做程序来写" | 如何架构设计复杂业务系统? 如何写复杂业务代码? Kotlin 开发者社区 "大师级程序员把 ...

  9. android单元测试作用,Android单元测试(二):再来谈谈为什么

    今天早上8点半坐到桌子前,打开电脑,看了几分钟体育新闻,做其他一些准备工作,到9点开始真正开始着手写这篇文章.于是开始google,找资料,打算列一大段冠冕堂皇的理由,来说明为什么要写单元测试,比如: ...

  10. 什么是单元测试(UnitTest),单元测试的作用是什么

    单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证.对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类, ...

最新文章

  1. intel xdk 打ios的ipa包
  2. 2017全球教育机器人行业研究报告(附PDF下载)
  3. Linux下shel脚本之批量修改文件扩展名
  4. sql语句之where子句
  5. 一个很简单的淡入淡出相册 (转)
  6. 怎么写字_写字楼外卖员不让进怎么办?写字楼外卖柜提供解决方案!
  7. 一套代码同时支持.NET Framework和.NET Core
  8. 消息中间件系列(四):消息队列MQ的特点、选型、及应用场景详解
  9. 官方认证:软件及信息技术从业者为新生代农民工
  10. 异步IO(来自博客园)
  11. Romoting 通信DEMO(整理)
  12. eclipse注释模板
  13. LINQ体验(2)——C# 3.0新语言特性和改进(上篇)
  14. C语言如何控制控制台窗口大小
  15. Android RecyclerView滑动即可删除和撤消
  16. 编程笔记:python中下划线的意义
  17. Redis详解(一)——基础知识与安装
  18. ugmented reality(AR) equipment
  19. 添加内核驱动模块(4)(mydriver.c+ Konfig+Makefile )
  20. 机器学习.周志华《15 规则学习 》

热门文章

  1. C++学习日记#2——幂法求矩阵的主特征值
  2. linux内核中的copy_to_user和copy_from_user(一)
  3. 写论文时,参考文献怎么引用?
  4. 台湾大学郭彦甫matlab百度云,台湾国立大学郭彦甫Matlab教程笔记(23) linear systems...
  5. 【Java】枚举类型
  6. java 堆内存结构_基于JDK1.8的JVM 内存结构【JVM篇三】
  7. Fortran 90:Fortran 学习笔记(一)
  8. 【图像融合】基于matlab高分辨率全色图IHS图像融合(含评价指标)【含Matlab源码 2406期】
  9. 单片机实现PT2262解码原理
  10. 青云上NAS服务器挂的操作(他们的文档)