Lab——代码测试指北

2023年 8月 22日 18.4k 0

💡 本文从代码测试的必要性,代码测试的流程,如何书写切片测试和单元测试四个角度进行了介绍,其中着重介绍了书写单元测试的大致流程,以及书写单元测试的每个步骤时所需要的方法,看完本篇文章你将会对代码测试有一个大致的了解,并可以实际操作给自己的代码进行单元测试。

测试代码是编写软件代码中重要的一环。作为软件开发人员不能觉得根据需求编写完代码就认为万事大吉了,也不能轻易的认为测试代码就是让程序跑起来;访问自己写的接口如果没有报错就测试通过了,测试代码应该是从代码逻辑到代码功能全方位的测试,只有保证代码逻辑和代码功能与预期均一致我们才能说我们编写的代码通过了代码测试。

🌟代码测试流程:

实验室编写代码的测试流程具体如下所示:当编写完代码后首先需要使用Alibaba静态代码检测工具来进行静态代码测试,这里的静态代码测试大多数指的是一种优雅的代码书写规范(比如驼峰命名法等等);

其次要进行白盒代码测试,白盒代码测试具体指的是深入到代码层面去测试代码的具体逻辑,只有保证书写的代码逻辑没有问题了才能保证这个代码所表示的功能没有问题;最后要进行黑盒测试,黑盒测试更多的是指使用接口访问工具(Apifox或者Postman)当给定预期的数据样例后接口能否返回给我们预期的JSON数据。当经历了整个代码测试流程我们才有信心说我们书写的代码没有问题了。

在整个测试流程中白盒测试因为涉及到代码逻辑的测试所以相对来说更加复杂也需要我们花费更多的时间来学习,下面我们将从如何书写白盒测试(单元测试,切片测试,集成测试)的角度来跟大家讲一讲如何书写好白盒测试。

🌟保证单元测试的有效性

单元测试代码覆盖率是验证单元测试是否有效的指标之一,但对我们来说更重要的是利用单元测试验证代码的逻辑——试图找到隐藏着的BUG。

  • 集成测试的特点:

    • 依赖外部环境和数据
    • 需要启动应用并初始化测试对象
    • 使用@Autowired注入测试对象
    • 无法验证不确定的返回值,只能靠打印日志来人工校对
  • 单元测试的特点:

    • 不依赖外部环境和数据
    • 不需要启动应用并初始化测试对象
    • 需要使用@Mock来初始化依赖对象,用@InjectMocks来初始化测试对象
    • 需要模拟出依赖方法,指定参数,返回值,异常等
    • 因为测试方法返回值确定,可以直接用Assert相关方法进行断言
    • 可以验证依赖方法的调用次数和参数值。

🌟白盒测试:

🌟🌟切片测试——Controller层:

Controller层切片测试的功能与使用Apifox,postman等接口测试工具测试比较相像,但在切片测试中进行Controller层测试的时候我们可以开启事务,如果你对数据库中数据的一致性比较在意那使用切片测试不失为一种好方法;

切片测试书写流程:

  • 使用MockMVCBuilder构造MockMvc的构造器
  • mockMvc调用perform,调用一个RequestBuilder请求,调用Controller的业务逻辑
  • 使用MockMvcRequestMatchers对返回的结果进行断言判断
  • 切片测试代码比较固定;具有例子如下所示:

    @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注解进行Mock:@Mock
  • 使用代码的方式进行Mock: Mockito.*mock*(UserInfoMapper.class)
  • 声明被测对象并使用注解 :@ InjectMocks
  • 把依赖对象注入被测对象的方法:

  • Whitebox.*setInternalState*(被测对象,"依赖对象名字",依赖对象);
  • 使用注解@InjectMock@Mock
  • @InjectMocks:定义的是被测对象,需要把其他依赖对象@Mock@Spy 注入到被测对象当中。
  • @Mock 定义的依赖对象
  • @Spy 定义的被测对象,定义的被测对象是真实调用的;需要配合@RunWith(PowerMockRunner.class)
  • 🌟模拟方法阶段具体操作

    1. 模拟(Mock)依赖方法:

    在执行单元测试过程中我们需要测试的测试方法会需要很多依赖方法,但我们并不想让这些依赖方法在执行单元测试的过程中真正的执行!所以在开始测试方法之前我们需要显示的告诉程序:当测试方法的途中执行了依赖方法时我们应该传给依赖方法什么参数,让依赖方法返回什么结果。

    😃 为什么要使用Mock(模拟)?

  • Mock可以解除外部依赖服务,保证了测试用例的独立性。
  • 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());
    

    💡 参考资料:

  • 链接:阿里云开发社区:Java单元测试实战
  • 相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论