单元测试规范 - 后端

背景:

  这是2021我在A项目时,响应公司号召,提高代码质量.

  经理要求我们开始写单元测试. 

  所以我写了这个规范出来.

  目前主要用的就是业务层的mock测试. 

单元测试规范

原则

单元测试文件必须拥有良好的结构和格式;

测试用例的分组名称和用例名称必须清晰易懂;

测试用例必须能描述测试目标的行为;

测试技术Mock

概念

在软件测试领域,Mock的意思是模拟,简单来说,就是通过某种技术手段模拟测试对象的行为,返回预先设计的结果。这里的关键词是预先设计,也就是说对于任意被测试的对象,可以根据具体测试场景的需要,返回特定的结果。打个比方,就像BBC纪录片里面的假企鹅,可以根据拍摄需要作出不同的反应。

作用

首先,Mock可以用来解除测试对象对外部服务的依赖(比如数据库,第三方接口等),使得测试用例可以独立运行。不管是传统的单体应用,还是现在流行的微服务,这点都特别重要,因为任何外部依赖的存在都会极大的限制测试用例的可迁移性和稳定性。可迁移性是指,如果要在一个新的测试环境中运行相同的测试用例,那么除了要保证测试对象自身能够正常运行,还要保证所有依赖的外部服务也能够被正常调用。稳定性是指,如果外部服务不可用,那么测试用例也可能会失败。通过Mock去除外部依赖之后,不管是测试用例的可迁移性还是稳定性,都能够上一个台阶。

Mock的第二个好处是替换外部服务调用,提升测试用例的运行速度。任何外部服务调用至少是跨进程级别的消耗,甚至是跨系统、跨网络的消耗,而Mock可以把消耗降低到进程内。比如原来一次秒级的网络请求,通过Mock可以降至毫秒级,整整3个数量级的差别。

Mock的第三个好处是提升测试效率。这里说的测试效率有两层含义。第一层含义是单位时间运行的测试用例数,这是运行速度提升带来的直接好处。而第二层含义是一个QE单位时间创建的测试用例数。如何理解这第二层含义呢?以单体应用为例,随着业务复杂度的上升,为了运行一个测试用例可能需要准备很多测试数据,与此同时还要尽量保证多个测试用例之间的测试数据互不干扰。为了做到这一点,QE往往需要花费大量的时间来维护一套可运行的测试数据。有了Mock之后,由于去除了测试用例之间共享的数据库依赖,QE就可以针对每一个或者每一组测试用例设计一套独立的测试数据,从而很容易的做到不同测试用例之间的数据隔离性。而对于微服务,由于一个微服务可能级联依赖很多其他的微服务,运行一个测试用例甚至需要跨系统准备一套测试数据,如果没有Mock,基本上可以说是不可能的。因此,不管是单体应用还是微服务,有了Mock之后,QE就可以省去大量的准备测试数据的时间,专注于测试用例本身,自然也就提升了单人的测试效率。

目录结构

  1. 创建 /src/test资源包,和/src/main同级
  2. test目录下的包结构和main下保持一致.如下图

依赖包

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>

Mockmvc控制层单测

特点

测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。

我们这里主要用MockMvc对流程性业务(业务不复杂的)进行测试.

实现原理

MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用

配置启动类要求

1. 类上加入包扫描,加入对应的启动类,自动配置MockMvc

2. 类中加入@Before注解 ,进行测试前的一些配置,比如登录获取token,设置用户全局变量等操作. 测试结束,加入@After注解,注明测试完毕或其他的一些操作.

3. 启动类作为其他具体测试类的父类.

启动类demo

@RunWith(SpringRunner.class)
@MapperScan(basePackages = "xxxxxx.mapper")
@SpringBootTest(classes = ClueApplication.class,webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class ApplicationTests {

@Before
public void init(){
System.out.println("开始测试-----------------");
login();
}

@After
public void after(){
System.out.println("测试结束-----------------");
}

}

测试类编写要求

1. 继承测试类启动类

2. 测试类类名命名方式为: 被测试类名+test , 如ClueControllerTest

3. 测试方法命名方式为:test+测试方法名, 如: testGetClueDetail()

4. 测试方法上加@Test注解.

5. 测试类上加@Transactional注解,会自动回滚.

6. 使用Assert的API,对返回结果进行断言.

7. 多种场景测试可以使用多次使用perform(x)方法进行测试,并断言.

注:如果是用的idea开发工具,可以直接在被测试的方法上,右键,然后GO TO -> test ,自动生成,如图:

测试类demo

GET请求的

@Test
public void testGetXxxxParam() throws Exception {
MvcResult result = mockMvc.perform(
MockMvcRequestBuilders.get("/clue/getXxxx/{id}",1)

.contentType(MediaType.APPLICATION_JSON_UTF8)
.header("auth_token",this.authToken)
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn();
//获取数据并断言
R r = JsonUtil.json2Object(result.getResponse().getContentAsString(), R.class);
List<GetClueParamVo> list = JsonUtil.json2List(JsonUtil.object2Json(r.getData()), GetXxxxParamVo.class);
Assert.assertEquals(0,r.getCode());
Assert.assertEquals(5,list.size());
}

POST请求的

@Test
public void getXxxxConfigs() throws Exception {
Page page = new Page();
page.setPageNum(1);
page.setPageSize(10);
String req = JsonUtil.object2Json(page);

//调用接口
MvcResult result = mockMvc.perform(
MockMvcRequestBuilders.post("/push/getXxxxConfigs")
.contentType(MediaType.APPLICATION_JSON)
.header("auth_token"this.authToken)
.content(req)
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn();
}

入参构建方式

无参数的get调用.

MvcResult result = mockMvc.perform(
MockMvcRequestBuilders.get("/clue/Xxxx")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.header("auth_token",this.authToken)
)

带参数的get调用.

MvcResult result = mockMvc.perform(
MockMvcRequestBuilders.get("/clue/getXxxx/{id}",1)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.header("auth_token",this.authToken)
)

带参数的post调用.

控制层接口入参是基本类型, 使用param传参,如下

mockMvc.perform(MockMvcRequestBuilders
.post("/ClueCollection/clueXxxxxx")
.header("auth_token"this.authToken)
.param("xx","xx")
.param("xx","xx")
.contentType(MediaType.ALL))

控制层接口入参是bean类型,使用content传参

//入参

Page page = new Page();
page.setPageNum(1);
page.setPageSize(10);
String req = JsonUtil.object2Json(page);

//调用接口
MvcResult result = mockMvc.perform(
MockMvcRequestBuilders.post("/push/getXxxxx")
.contentType(MediaType.APPLICATION_JSON)
.header("auth_token"this.authToken)
.content(req)
)

初始化数据
  1. 创建mapper接口;
  2. 在resouces下创建mapper包,写对应的mapper.xml
  3. 在测试类中注入,直接使用即可.
执行结果说明

1. andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确。

2. andDo:添加ResultHandler结果处理器,比如调试时打印结果到控制台。

3. andReturn:最后返回相应的MvcResult;然后进行自定义验证/进行下一步的异步处理。

Mockito业务层单测;

对于业务逻辑复杂的接口,使用Mockito进行单元测试;

特点

将注意力放在业务逻辑上,而不依赖数据库.

实现原理

通过CGLib在运行时为每一个被Mock的类或者对象动态生成一个代理对象,返回预先设计的结果

集成步骤

  1. 标记被Mock的类或者对象,生成代理对象
  2. 通过Mockito API定制代理对象的行为
  3. 调用代理对象的方法,获得预先设计的结果

Mockito用法示例

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { ClueApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ClueCollRelServiceTest {

@MockBean
private TGzmxClueClueCollectionRelationMapper mapper;

@Autowired
private ClueCollRelService clueCollRelService;

@Test
public void updateCollClue() {
//mock数据
when
(mapper.updateById(isNotNull())).thenReturn(1);
//入参
CollClueUpdateDto dto = new CollClueUpdateDto();
dto.setId(1L);
dto.setAdLegalBasis("abc");
//测试
int i = clueCollRelService.updateCollClue(dto);
//断言
assertEquals
("更新失败",1, i);
}
}

使用断言的几个原则

  1. 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
  2. 使用断言对函数的参数进行确认。
  3. 在编写函数时,要进行反复的考查,并且自问:"我打算做哪些假定?"一旦确定了的假定,就要使用断言对假定进行检查。
  4. 当进行防错性编程时,如果"不可能发生"的事情的确发生了,则要使用断言进行报警。

后续补充:

  1. 如何对无返回值的mapper进行mock?

如图:

使用doNothing来mock,就是不做任何操作;

  1. 其他的mock方式

可以用dothrow也行,抛异常测试.

  1. 如果被测试的service.XXX没有返回值怎么断言?

使用verify来断言执行的次数.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值