💡 本文从代码测试的必要性,代码测试的流程,如何书写切片测试和单元测试四个角度进行了介绍,其中着重介绍了书写单元测试的大致流程,以及书写单元测试的每个步骤时所需要的方法,看完本篇文章你将会对代码测试有一个大致的了解,并可以实际操作给自己的代码进行单元测试。
测试代码是编写软件代码中重要的一环。作为软件开发人员不能觉得根据需求编写完代码就认为万事大吉了,也不能轻易的认为测试代码就是让程序跑起来;访问自己写的接口如果没有报错就测试通过了,测试代码应该是从代码逻辑到代码功能全方位的测试,只有保证代码逻辑和代码功能与预期均一致我们才能说我们编写的代码通过了代码测试。
🌟代码测试流程:
实验室编写代码的测试流程具体如下所示:当编写完代码后首先需要使用Alibaba静态代码检测工具来进行静态代码测试,这里的静态代码测试大多数指的是一种优雅的代码书写规范(比如驼峰命名法等等);
其次要进行白盒代码测试,白盒代码测试具体指的是深入到代码层面去测试代码的具体逻辑,只有保证书写的代码逻辑没有问题了才能保证这个代码所表示的功能没有问题;最后要进行黑盒测试,黑盒测试更多的是指使用接口访问工具(Apifox或者Postman)当给定预期的数据样例后接口能否返回给我们预期的JSON数据。当经历了整个代码测试流程我们才有信心说我们书写的代码没有问题了。
在整个测试流程中白盒测试因为涉及到代码逻辑的测试所以相对来说更加复杂也需要我们花费更多的时间来学习,下面我们将从如何书写白盒测试(单元测试,切片测试,集成测试)的角度来跟大家讲一讲如何书写好白盒测试。
🌟保证单元测试的有效性
单元测试代码覆盖率是验证单元测试是否有效的指标之一,但对我们来说更重要的是利用单元测试验证代码的逻辑——试图找到隐藏着的BUG。
-
集成测试的特点:
- 依赖外部环境和数据
- 需要启动应用并初始化测试对象
- 使用@Autowired注入测试对象
- 无法验证不确定的返回值,只能靠打印日志来人工校对
-
单元测试的特点:
- 不依赖外部环境和数据
- 不需要启动应用并初始化测试对象
- 需要使用@Mock来初始化依赖对象,用@InjectMocks来初始化测试对象
- 需要模拟出依赖方法,指定参数,返回值,异常等
- 因为测试方法返回值确定,可以直接用Assert相关方法进行断言
- 可以验证依赖方法的调用次数和参数值。
🌟白盒测试:
🌟🌟切片测试——Controller层:
Controller层切片测试的功能与使用Apifox,postman等接口测试工具测试比较相像,但在切片测试中进行Controller层测试的时候我们可以开启事务,如果你对数据库中数据的一致性比较在意那使用切片测试不失为一种好方法;
切片测试书写流程:
切片测试代码比较固定;具有例子如下所示:
@SpringBootTest(classes = EEApplication.class)
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@Transactional
public class BackstageControllerTest {
@Resource
private MockMvc mockMvc;
@Autowired
private BackstageController backstageController;
@Before
public void setup(){
mockMvc = MockMvcBuilders.standaloneSetup(backstageController).build();
}
@Test
public void register() throws Exception {
String username = "SunZYY";
String password = "123456";
String role = "ROLE_ADMIN";
// 请求路径,不需要写协议、服务器主机和端口号
String url = "/bac/register";
// 执行测试
// 以下代码相对比较固定
this.mockMvc.perform(
// 根据请求方式决定调用的方法
MockMvcRequestBuilders.post(url)
// 请求数据的文档类型,例如:application/json;
// application/x-www-form-urlencoded;charset=UTF-8
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("username", username) // 请求参数,有多个时,多次调用param()方法
.param("password", password)
.param("role",role)
.accept(MediaType.APPLICATION_JSON))
// 接收的响应结果的文档类型,注意:perform()方法到此结束
.andExpect( // 预判结果,类似断言
MockMvcResultMatchers
.jsonPath("msg") // 预判响应的JSON结果中将有名为state的属性
.value("用户注册成功,请登录"))
//预判响应的JSON结果中名为state的属性的值,注意:andExpect()方法到此结束
.andDo( // 需要执行某任务
MockMvcResultHandlers.print());
}
🌟🌟单元测试:
😃 单元测试命名规范:
当一个方法有多个测试用例时,需要创建多个测试方法: 测试方法+With+条件 表达不同的测试用例方法
例子:testMethodWithSuccess,testMethodWithFailure ,testMethodWithException
单元测试书写流程:
🌟定义对象阶段具体操作
1.模拟依赖对象,定义被测对象,把依赖对象注入到测试对象中
通常来讲被测对象有许多依赖对象,我们需要对被测对象的某个方法进行测试,我们会发现需要测试的这个方法并不是需要被测对象的所有依赖,所以我们首先需要把被测对象中需要的依赖对象找出来进行Mock(模拟)。【Mock(模拟):创建一个虚拟的对象来进行测试,当需要使用这个虚拟对象时不会真实进行调用,而是根据我们自定义的值来进行返回】
对被测方法的依赖对象进行Mock(模拟)的方法有两种:
@Mock
Mockito.*mock*(UserInfoMapper.class)
@ InjectMocks
把依赖对象注入被测对象的方法:
Whitebox.*setInternalState*(被测对象,"依赖对象名字",依赖对象);
@InjectMock
和@Mock
@InjectMocks
:定义的是被测对象,需要把其他依赖对象@Mock
,@Spy
注入到被测对象当中。@Mock
定义的依赖对象@Spy
定义的被测对象,定义的被测对象是真实调用的;需要配合@RunWith(PowerMockRunner.class)
🌟模拟方法阶段具体操作
1. 模拟(Mock)依赖方法:
在执行单元测试过程中我们需要测试的测试方法会需要很多依赖方法,但我们并不想让这些依赖方法在执行单元测试的过程中真正的执行!所以在开始测试方法之前我们需要显示的告诉程序:当测试方法的途中执行了依赖方法时我们应该传给依赖方法什么参数,让依赖方法返回什么结果。
😃 为什么要使用Mock(模拟)?
依赖对象的参数可以显示给出:
//Mockito.doReturn(返回参数).when(依赖对象).依赖对象的方法(依赖对象方法需要的参数,依赖对象方法需要的参数);
Mockito.doReturn(null).when(userInfoMapper).getUserInfoByNameAndRole(paramUserName,paramRole);
Mockito.doReturn(paramEncoderPassword).when(passwordEncoder).encode(paramPassword);
依赖对象的参数无法直接指定:(具体参考下面的模拟以来所需要的参数板块)
❗注意:依赖方法的参数要不然就是全部自己给定,要不然就要全部使用Mockito.eq或捕获器进行定义不可以既自己给定又使用Mockito.eq进行模拟。
//Mockito.doReturn(返回参数).when(依赖对象).依赖对象的方法(依赖对象需要的参数,依赖对象需要的参数);
Mockito.doReturn(1).when(userInfoMapper).insertUser(Mockito.eq(paramUserName),
Mockito.eq(paramEncoderPassword), stringCaptor.capture(),
Mockito.eq(paramRole),stringCaptor.capture(),integerCaptor.capture())
2. 模拟依赖所需要的参数:
模拟依赖所需要的参数有两种情况:
依赖对象的某些参数我们并不能够在编写单元测试时我们自己给出,这些参数根据代码的不同逻辑会是不同的值。当出现这种情况时我们就需要使用参数捕获器来模拟依赖对象所需要的参数,在验证方法阶段也需要对模拟的依赖参数进行验证。
定义捕获器:
按照需要捕获的类型不同定义不同类型的捕获器:
//String 类型
ArgumentCaptor stringCaptor = ArgumentCaptor.forClass(String.class);
//Integer 类型
ArgumentCaptor integerCaptor = ArgumentCaptor.forClass(Integer.class);
使用捕获器:
stringCaptor.capture()
integerCaptor.capture()
验证需要捕获的依赖参数是否和预期一致:
a. 如果捕获器捕获了多个值:首先需要用List来接受捕获到的多个值;List中的顺序与捕获的顺序相同
```java
List allValues = stringCaptor.getAllValues();
Integer proof_number = integerCaptor.getValue();
Assert.assertEquals(allValues.get(0),ADMIN_AUTH);
Assert.assertEquals(allValues.get(1),ROLE_ADMIN);
```
b. 捕获器只捕获了一个值直接进行断言判断:
Integer proof_number = integerCaptor.getValue();
Assert.assertEquals(proof_number,Integer.valueOf(0));
🌟调用方法阶段具体操作
调用测试方法阶段很简单,我们已经在定义对象阶段定义好了需要测试的对象,只需要正常测试方法即可。
🌟验证方法阶段具体操作
验证被测方法返回的数据对象是否符合预期:采用Assert断言的方式进行
//Assert.assertEquals(期待返回值,真实返回值);
Assert.assertEquals(queryResponse,testQueryResponse);
验证依赖参数是否正确(见模拟依赖所需要的参数部分)
验证依赖方法是否被调用:
//Mockito.verify(依赖对象).依赖方法(方法所需要的参数,方法所需要的参数);
Mockito.verify(userInfoMapper).getUserInfoByNameAndRole(paramUserName,paramRole);
单元测试的一种特殊情况:
当我们的项目使用了全局异常处理之后,我们在书写逻辑代码时一些特殊的我们都会使用全局异常处理去处理,让异常上抛到Controller层,由Controller层来进行情况处理后返回给前端JSON数据;此时我们进行Service层单元测试时就直接判断抛出的异常是否为我们自定义的异常即可。
具体的单元测试流程和上述流程并无二致:
【进行异常的单元测试时:调用测试方法和检验异常类型和异常消息是一起进行的】具体代码如下所示:】
//.调用方法阶段
EventExtractionException expectedException = Assert.assertThrows("异常类型不一致", EventExtractionException.class, () -> backstageService.register(userName, password, role));
//4.验证异常消息是否一致
Assert.assertEquals("异常消息不一致","用户名已经被注册,请更换用户名",expectedException.getMessage());
💡 参考资料: