当前位置:网站首页>为什么要写单元测试?如何写单元测试?
为什么要写单元测试?如何写单元测试?
2022-07-21 04:27:00 【小码哥说测试】
01、为什么要写单元测试
一聊起测试用例,很多人第一反应就是,我们公司的测试会写测试用例的,我自己也会使用postman或者swagger之类的进行代码自测。那我们研发到底要不要写单元测试用例呢?参考阿里巴巴开发手册,第8条规则(单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率都要达到 100%),大厂的要求就是必须喽。我个人感觉,写单元测试用例也是很有必要的,好处很多,例如:
保证代码质量!!!无论初级,中级,高级攻城狮开发工程的代码,且不说效率如何,功能是必要要保证是正确的;交付测试以后,bug锐减,联调飞快。
代码逻辑“文档化”!!!新人接手维护模块代码时,通过单元测试用例,以debug的方式就能熟悉业务代码。比起,看代码,研究表结构梳理代码结构,效率提升飞快。
易维护!!!新人接手维护代码模块时,提交自己的代码时,远行之前的单元测试达到回归测试,保证了新改动不会影响老业务。
快速定位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<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的单元测试用例,编写两类,如下:
业务参数的校验,和义务异常的校验。例如,名称是否为空,电话号码是否正确,用户未登陆则抛出未登陆异常。
各类业务场景的真实测试用例,例如,编写成功添加顶级菜单的测试用例,已经编写成功添加子级菜单的测试用例。
注意事项
配置覆盖
此外,如上基于mockmvc的编写的测试用例,由于加载了Spring的配置,会对项目发起真实的调用。如果,环境的配置为线上配置,容易出现安全问题;一般,处于安全考虑,很多公司会对真实环境的修改操作做事务回滚操作,甚至根本就不会进行真实环境的调用,使用模拟环境替换,例如数据库的操作可以使用h2内存数据库进行替换。
这时,可以在src/test/resources目录下,添加与src/main/resources目录下,相同的文件进行配置覆盖。src/test/main目录下的代码,会首先加载src/test/resources目录下的配置,如果没有则在加载src/main/resources目录的配置。常用场景如下:
在单元测试环境使用使用内存数据库。
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();
}
}
绵薄之力
最后感谢每一个认真阅读我文章的人,看着粉丝一路的上涨和关注,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走
这些资料,对于在从事【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!凡事要趁早,特别是技术行业,一定要提升技术功底。希望对大家有所帮助…….
边栏推荐
- Lexin esp-rtc real-time audio and video communication scheme
- Understand the differences between mongodb and MySQL
- 蚓激酶白蛋白纳米粒/红细胞膜定向包裹血红蛋白-白蛋白纳米粒的研究制备
- 常用功能测试的检查点与用例设计思路
- Siyuan synchronization problem: cloud object not found v2.1.0
- Attack and defense world ----- lottery
- 受众分析与卸载分析全面升级,HMS Core分析服务6.6.0版本上新
- 无线定位技术实验二 TDOA最小二乘定位法
- 使用JPofiler工具分析OOM原因
- Aardio - function execution failed retry test
猜你喜欢
紫杉醇联合2-甲氧基雌二醇的白蛋白纳米粒/荜茇酰胺白蛋白纳米粒的研究制备
Installing redis in Linux
ASTM F 814 test method for specific optical density of smoke produced by solid materials for aerospace equipment
笔试强训第20天
Es custom analyzer
Introduction to ESP privilege isolation mechanism
The 22 pictures show you in-depth analysis of prefix, infix, suffix expressions and expression evaluation
Redis+caffeine two-level cache enables smooth access speed
PWM输出实验
深度学习—— (5)数据不平衡之class_weight
随机推荐
(1) Analysis of Damon database model
受众分析与卸载分析全面升级,HMS Core分析服务6.6.0版本上新
Verilog grammar basics HDL bits training 03
Insert sort code
动作活体检测能力,构建安全可靠的支付级“刷脸”体验
Preparation of hemoglobin albumin nanoparticles encapsulated by Lumbrokinase albumin nanoparticles / erythrocyte membrane
【C 练习】宏实现交换数字二进制奇偶位
动态内存管理2之柔性数组
mysql安装提示应用程序无法正常启动(0xc000007b,如何解决?
深度学习——(6)pytorch冻结某些层的参数
OKR case of personnel department: create the best office environment for colleagues
Ripple test of DC DC switching power supply
Web3流量聚合平台Starfish OS,给玩家元宇宙新范式体验
Classic examples of C language: 21-30 examples: insertion sort, Hill sort 1, quick sort, Hill sort 2, recursion, completion, Fibonacci sequence, common divisor and common multiple, judging the number
Audience analysis and uninstall analysis have been comprehensively upgraded, and HMS core analysis service version 6.6.0 has been updated
Preparation of dihydrotanshinone I loaded albumin nanoparticles / norcantharidin albumin nanoparticles / voriconazole albumin nanoparticles
Understand the differences between mongodb and MySQL
NFS share
常用功能测试的检查点与用例设计思路
蚓激酶白蛋白纳米粒/红细胞膜定向包裹血红蛋白-白蛋白纳米粒的研究制备