软件测试项目中如何做好单元测试

news2024/12/22 20:50:07

前言

如《Unit Testing》书里提到,学习单元测试不应该仅仅停留在技术层面,比如你喜欢的测试框架,mocking 库等等,单元测试远远不止「写测试」这件事,你需要一直努力在单元测试中投入的时间回报最大化,尽量减少你在测试中投入的精力,并最大化测试提供的好处,实现这两点并不容易。

和我们在日常开发中遇到的问题一样,学会一门语言,掌握一种方法并不困难,困难的是把投入的时间回报最大化。unit test有很多基础知识和框架,在google上一搜就一大堆,最佳实践的方法论也非常多,本文不准备讨论这些问题,而是结合在我们日常的工作,讨论如何使用好单元测试这把武器。

单元测试的定义

什么是单元测试?来自百度

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。至于【单元】的含义,一般来说,要根据实际情况判定具体含义,如Java里单元指一个类等。

讲人话,单元测试就是为了验证一个类的准确性的测试。区别于集成测试和系统测试。他是前置的,由开发人员主导的最小规模的测试。

一些学者们经过统计,还绘制出了下图:

·85%的缺陷都在代码设计阶段产生;

· 发现bug的阶段越靠后,耗费成本就越高,呈指数级别的增长。

由此看来,单测代码的编写对于交付质量以及人工耗费成本都有极其重要的影响。

常见的误区

浪费时间,影响开发速度

不同项目的开发测试时间曲线不同,你要综合考虑你的代码的生命周期,你debug的能力,你平时花多少时间review有问题的代码。随着项目的进行,这些时间会递增,如果你想你所写的代码能够一直用下去,不让后来人吐槽这写的什么玩意,单元测试非常有必要。

测试应该是测试的工作

开发是代码的第一责任人,最熟悉代码的人,在设计阶段编辑单元测试,不但可以让你更自信的交付,还可以减少测试问题的产生。同时你自己的全栈能力也有所提升。

代码不是我写的,我不懂

我们经常抱怨老代码有坑难懂,或者是缺乏CR。其实在编写单元测试的过程中,也是CR和学习的一个过程,对于代码的主流程,边界,异常等有了深入的理解。同时也是自我审视代码规范、逻辑、设计的过程。我建议在重构中写单测,在写单测中重构,相辅相成。

如何写出好的单测

方法论上,有AIR 原则 ,像空气一样不会被感受到即 Automatic(自动化)、Independent(独立性)、Repeatable(可重复)。

我个人的理解就是:

1、自动运行,通过CI集成的方式,保证单测能够自动运行,通过assert保证单元测试的验证结果,而不是print输出。确保单元测试能够自动化运行,不需要人工介入测试。

2、单元测试必须独立,不能互相调用,也不能有依赖的顺序。每个测试用例之间包保证独立。

3、不可以受运行的环境、数据库、中间件等影响。在编写单测的时候,需要把外部的依赖mock掉。

从覆盖率的规范上来讲,不管是阿里内部还是业界,都有很多标准。

语句覆盖率达到70%;核心模块的语句覆盖率和分支覆盖率都要达到100%。 --- 《阿里巴巴Java开发手册》

单测覆盖度分级参考

Level1:正常流程可用,即一个函数在输入正确的参数时,会有正确的输出

Level2:异常流程可抛出逻辑异常,即输入参数有误时,不能抛出系统异常,而是用自己定义的逻辑异常通知上层调用代码其错误之处

Level3:极端情况和边界数据可用,对输入参数的边界情况也要单独测试,确保输出是正确有效的

Level4:所有分支、循环的逻辑走通,不能有任何流程是测试不到的

Level5:输出数据的所有字段验证,对有复杂数据结构的输出,确保每个字段都是正确的

从上面的摘录看,语句覆盖率和分支覆盖率都有数值上和方法论上的要求,那在实际工作中,实践情况如何呢?

笔者曾在一个季度,工作中提交的代码综合增量覆盖率几乎达到了100%。我可以谈谈我的经验和实践。

60%左右的单测覆盖率可以非常轻松达到,但达到95%以上的覆盖率,需要覆盖各种代码分支和异常情况等,甚至是配置和bean的初始化方法,所投入的时间非常巨大,但边际效应递减。我想测试toString, getter/setter这样的方法也没有意义。多少合适,我认为没有一个固定的标准。高代码覆盖率百分比不表示成功,也不意味着高代码质量。该舍弃测试的部分就大胆的ignore掉。

最佳实践

这个标题未免有些标题党。单元测试相关的书籍、ata文章,数不胜数,我的所谓“最佳实践”是在实际阿里工作中的一些自己踩过的坑,或者我个人认为一些重要的点,班门弄斧,如有错误,欢迎讨论。

1、隐藏的测试边界值

public ApiResponse<List<Long>> getInitingSolution() {
      List<Long> solutionIdList = new ArrayList<>();
      SolutionListParam solutionListParam = new SolutionListParam();
      solutionListParam.setSolutionType(SolutionType.GRAPH);
      solutionListParam.setStatus(SolutionStatus.INIT_PENDING);
      solutionListParam.setStartId(0L);
      solutionListParam.setPageSize(100);
      List<OperatingPlan> operatingPlanList =  operatingPlanMapper.queryOperatingPlanListByType(solutionListParam);
      for(; !CollectionUtils.isEmpty(operatingPlanList);){
          /*
              do something
              */
          solutionListParam.setStartId(operatingPlanList.get(operatingPlanList.size() - 1).getId());
          operatingPlanList =  operatingPlanMapper.queryOperatingPlanListByType(solutionListParam);
      }
      return ResponsePackUtils.packSuccessResult(solutionIdList);
  }

上面这段代码,如何写单元测试?

很自然的,我们写单测的时候会mock掉数据库查询,并且查出信息。但是如果查询的内容超过100,由于for循环进入一次,无法通过jacoco的自动覆盖率发现。实际上没有覆盖这个边界case,只能通过开发者的习惯来处理这些边界情况。

如何处理这些隐藏的边界值,开发者不能依赖集成测试或者代码CR,必须要在自己写单元测试的时候考虑到这一情况,能避免后来维护的人掉坑。

2、不要在springboot测试中使用@Transactional以及操作真实数据库

单元测试的上下文应该是干净的,设计transactional的初衷是为了集成测试(如spring官网介绍):

虽然直接操作DB能更容易验证DAO层的正确性,但是也容易被线下数据库的脏数据污染,导致单测无法通过的问题。笔者以前遇到直连数据库的单测代码,经常改个5分钟代码,数据库里脏数据清一个小时。第二就是集成测试需要启动整个应用的容器,违背了提高效率的初衷。

如果实在要测DAO层的正确性,可以整合H2嵌入式数据库。这个网上教程非常多,不再赘述。

3、单测里时间相关的内容

笔者曾经在工作中遇到过一个极端case,一个CI平时都正常运行,有一次深夜发布, CI跑不过,后来经过第二天check才发现有前人在单测中取了当前时间,在业务逻辑中含有夜间逻辑(夜间消息不发),导致了CI无法通过。那么时间在单测中要如何处理呢?

在使用Mockito时,可以使用mock(Date.class)来模拟日期对象,然后使用when(date.getTime()).thenReturn(time)来设置日期对象的时间。

如果你使用了calendar.getInstance(),如何获取当前时间?Calendar.getInstance()是static方法,无法通过Mockito进行mock。需要引入powerMock,或者升级到mockito 4.x才能支持:

@RunWith(PowerMockRunner.class)
  @PrepareForTest({Calendar.class, ImpServiceTest.class})   
  public class ImpServiceTest {
      @InjectMocks
      private ImpService impService = new ImpServiceImpl();
      @Before
      public void setup(){
          MockitoAnnotations.initMocks(this);
          Calendar now = Calendar.getInstance();
          now.set(2022, Calendar.JULY, 2 ,0,0,0);
          PowerMockito.mockStatic(Calendar.class);
          PowerMockito.when(Calendar.getInstance()).thenReturn(now);
      }
  }

4、final类,static类等的单元测试

如第3点提到的calendar的例子,static类的mock需要mockito4.x的版本。否则就要引入powermock,powermock不兼容mockito3.x版本,不兼容mockito 4.x版本。由于老的应用引入了非常多的mockito3.x的版本,直接使用mockito4.x对final和static类进行mock需要排包。实践中看,[url=]JUnit[/url]、Mockito、Powermock三者之间的版本号有兼容性问题,可能会出现java.lang.NoSuchMethodError,需要根据实际的情况选择版本进行mock。

但是在新项目立项的时候,要确定好使用的mockito和junit版本,是否引入powermock等框架,确保环境稳定可用。老项目建议不要大规模改动mockito和powermock的版本,容易排包排到怀疑人生。

5、应用启动报 Can not load this fake sdk class 的异常

这是因为阿里的tair,metaq基于pandora容器的,fake-sdk默认是pandora模块类加载加载的。具体原理可以参考下图:

 

解决方案1,引入pandoraboot环境。

@RunWith(PandoraBootRunner.class)

这样其实减慢了单测的运行速度,是违背了高效性原理的。但是相比较运行整个容器,运行pandora容器的时间大概在10s左右,还是能够容许的。

那么有没有不让pandoraboot起来,纯mock的方法。我个人认为mock要比ut更优先 ,特别是有些外部依赖,经常迁移或者下线,可能改了1行代码,需要修1个小时测试用例。tair,lindorm等中间件也没有办法本地起环境进行mock,直接依赖外部资源非常不优雅。

解决方案2,直接mock

以tair为例:

@RunWith(PowerMockRunner.class)
  @PrepareForTest({DataEntry.class})
  public class MockTair {
      @Mock
      private DataEntry dataEntry;
      @Before
      public void hack() throws Exception {
          //solve it should be loaded by Pandora Container. Can not load this fake sdk class. please refer to http://gitlab.alibaba-inc.com/mi ... dora-boot/wikis/faq for the solution
          PowerMockito.whenNew(DataEntry.class).withNoArguments().thenReturn(dataEntry);
      }

      @Test
      public void mock() throws Exception {
          String value = "value";
          PowerMockito.when(dataEntry.getValue()).thenReturn(value);
          DataEntry tairEntry = new DataEntry();
          //值相等
          Assert.assertEquals(value.equals(tairEntry.getValue()));
      }
  }

6、metaq怎么写单测

MessageExt的mock方法参考5,但是单测中怎么运行一个MetaPushConsumer的bean,并调用listener方法。那就只能启动context的上下文。托管SpringRunner的方式。

@RunWith(PandoraBootRunner.class)
  @DelegateTo(SpringRunner.class)
  public class EventProcessorTest {
      @InjectMocks
      private EventProcessor eventProcessor;
      @Mock
      private DynamicService dynamicService;
      @Mock
      private MetaProducer dynamicEventProducer;
      @Test
      public void dynamicDelayConsumer() throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
          //获取bean
          MetaPushConsumer metaPushConsumer = eventProcessor.dynamicEventConsumer();

          //获取Listener
          MessageListenerConcurrently messageListener = (MessageListenerConcurrently)metaPushConsumer.getMessageListener();
          List<MessageExt> list = new ArrayList<>();

          //这个需要依赖PandoraBootRunner
          MessageExt messageExt = new MessageExt();
          list.add(messageExt);
          Event event = new Event();
          event.setUserType(3);
          String text = JSON.toJSONString(event);
          messageExt.setBody(text.getBytes());
          messageExt.setMsgId(""+System.currentTimeMillis());

          //测试consumeMessage方法
          messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
          doThrow(new RuntimeException()).when(dynamicService).triggerEventV2(any());
          messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
          messageExt.setBody(null);
          messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
      }
  }

总结一下什么时候使用容器:

// 1. 使用PowerMockRunner
  @RunWith(PowerMockRunner.class)
  // 2.使用PandoraBootRunner, 启动pandora,使用tair,metaq等
  @RunWith(PandoraBootRunner.class)
  // 3. springboot启动,加入context上下文,可以直接获取bean
  @SpringBootTest(classes = {TestApplication.class})

7、尽量使用ioc

使用 IOC 可以解耦对象,使得测试更加方便。经常有这样的情况,在某个 service 中使用到某个工具类,这个工具类内的方法都是 static 的,这样的话,测试 service 的时候就会需要连着工具类一起测试了。

比如下面这段代码:

@Service
  public class LoginServiceImpl implements LoginService{
      public Boolean login(String username, String password,String ip) {
          // 校验ip
          if (!IpUtil.verify(ip)) {
              return false;
          }
          /*
            other func
          */
          return true;
      }
  }

通过IpUtil校验登录用户的ip信息,而如果我们这样使用,就需要测试 IpUtil的方法, 违背了隔离性的原则。测试login方法也需要加入更多组测试数据覆盖工具类代码,耦合度太高。

如果稍加修改:

 @Service
  public class LoginServiceImpl implements LoginService{
      public Boolean login(String username, String password,String ip) {
          // 校验ip
          if (!IpUtil.verify(ip)) {
              return false;
          }
          /*
            other func
          */
          return true;
      }
  }

这样我们只需要单独测试IpUtil类和LoginServiceImpl类就行了。测试LoginServiceImpl的时候mock掉IpUtil就可以了,这样就隔离了IpUtil的实现。

8、不要为了覆盖率测没意义的代码

比如toString,比如getter,setter,都是机器生成的代码,单测没意义。如果是为了整体测试覆盖率的提高,那么请在CI中排掉这部分包:

 9、如何测试void方法

·如果void方法内部造成了数据库的变更,比如insertPlan(Plan plan),并通过H2操作过数据库,那么可以验证数据库的条数变化等,校验void方法的正确性。

· 如果void方法调用了函数,可以通过verify验证方法得到调用次数:

userService.updateName(1L,"qiushuo");
  verify(mockedUserRepository, times(1)).updateName(1L,"qiushuo");

· 如果void方法可能会造成抛出异常。

可以通过dothrow来 mock方法抛出的异常:

@Test(expected = InvalidParamException.class)
  public void testUpdateNameThrowExceptionWhenIdNull() {
     doThrow(new InvalidParamException())
        .when(mockedUserRepository).updateName(null,anyString();
     userService.updateName(null,"qiushuo");
  }

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

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/683345.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

kotlin Flow系列之-SharedFlow

文章目录 前言SharedFlow之创建SharedFlow之缓存系统buffer&Slots SharedFlow源码发送数据接收数据SharedFlow存在的bug 前言 Kotlin中Flow被分为冷流 热流 两大类。比如经常被使用的flow{}函数就可以创建一个冷流。而本文的主角SharedFlow就是一个热流。冷流需要调用Flow…

Makefile基本原理详解及使用

1、什么是 Makefile 一个企业级项目&#xff0c;通常会有很多源文件&#xff0c;有时也会按功能、类型、模块分门别类的放在不同的目录中&#xff0c;有时候也会在一个目录里存放了多个程序的源代码。 这时&#xff0c;如何对这些代码的编译就成了个问题。Makefile 就是为这个问…

电路图中常见符号总结

前辈说不会FPGA电路原理图&#xff0c;就不能知道如何去控制、如何去实现 因此本篇记录看的原理图中见到的符号&#xff0c;虽然都很基础&#xff0c;但我都不会&#xff0c;难受&#xff0c;因此只能看一点记一点 >_< >_< >_< >_< >_&…

如何学习和提升软件测试与调试的能力?

要学习和提升软件测试和调试能力&#xff0c;可以考虑以下方法&#xff1a; 学习软件测试基础知识&#xff1a; 了解软件测试的基本概念、原则和方法。可以通过阅读相关书籍、参加在线课程或培训来学习软件测试的基础知识。 掌握测试技术和工具&#xff1a; 了解各种测试技术…

南卡OE Pro新品上线即刻售罄:秒空背后的热潮是什么?

近日&#xff0c;南卡OE Pro品牌的最新产品上线&#xff0c;却在瞬间被抢购一空&#xff0c;引起了广大耳机爱好者的热烈关注和激烈讨论。这一现象再次展示了南卡OE Pro在耳机市场的强劲号召力和深受用户喜爱的地位。让我们一起揭开这场秒空背后的热潮&#xff0c;探究引发如此…

全网首个Ozon选品数据分析软件重磅上线!

【萌啦OZON数据】分析工具重磅上线&#xff01;在线解析俄罗斯市场、开启热销选品新玩法&#xff01;1对1免费指导&#xff0c;详细演示软件选品&#xff0c;快速上手&#xff01; 欧洲第四大电商市场OZON 近日&#xff0c;俄罗斯电商平台Ozon2023年Q1业绩新鲜出炉。数据显示&a…

OpenStack(T版)基础环境准备

文章目录 OpenStack(T版)基础环境准备centos硬件安装版本安装两台centos7服务器虚拟网络编辑器配置网络修改主机名主机名映射关闭防火墙和SElinux配置时间服务器安装openstack(T版)包安装SQL数据库(controller)安装消息队列rabbitmq(controller)安装分布式内存对象缓存系统memc…

代码随想录算法训练营第46天 | 139.单词拆分 + 多重背包理论基础 + 背包问题总结

今日任务 目录 139.单词拆分 - Medium 多重背包理论基础 背包问题总结 递推公式 遍历顺序 139.单词拆分 - Medium 题目链接&#xff1a;力扣-139. 单词拆分 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。 注意…

操作系统——实现一个简单的 shell 命令行解释器

一&#xff1a;实验题目 实现一个简单的 shell 命令行解释器 二&#xff1a;实验目的 本实验主要目的在于进一步学会如何在 Linux 系统下使用进程相关的系统调用&#xff0c;了解 shell 工作 的基本原理&#xff0c;自己动手为 Linux 操作系统设计一个命令接口。 三&…

WebSocket是什么,怎么用

74. WebSocket是什么&#xff0c;怎么用 当涉及到实时通信和即时更新的需求时&#xff0c;WebSocket 是一种强大且常用的解决方案。 1. 什么是 WebSocket&#xff1f; WebSocket 是一种在客户端和服务器之间实现双向通信的协议。它允许服务器主动向客户端推送数据&#xff0…

在Mac上安装Aspectj1.9.8(用于Java17)

1. 确定所使用的Java版本和AspectJ的对应关系 2. 下载AspectJ包 3. 安装AspectJ 4. 添加AspectJ对应的环境变量 5. 测试AspectJ 5.1 简单命令行测试安装是否成功 5.2 在IDEA中添加对AspectJ的支持【todo】 5.3 敲测试代码并进行测试【todo】 -----------------------------…

如何将window文件夹挂载到VMware系统mnt目录

背景&#xff1a;项目开发过程中&#xff0c;通常是在Windows上编码&#xff0c;有些框架和软件只能够在Linux上面执行&#xff0c;如果在 VMware中的Linux上面开发不太方便&#xff0c;因此需要在Windows上面开发好再同步到Linux上面运行。 软件&#xff1a; Samba客户端 V…

nodejs安装及环境变量配置(修改全局安装依赖工具包和缓存文件夹及npm镜像源)

本机环境&#xff1a;win11家庭中文版 一、官网下载 二、安装 三、查看nodejs及npm版本号 1、查看node版本号 node -v 2、查看NPM版本号&#xff08;安装nodejs时已自动安装npm&#xff09; npm -v 四、配置npm全局下载工具包和缓存目录 1、查看安装目录 在本目录下创建no…

抓包技术的应用示例

前言 最近瑞幸在搞活动&#xff0c;每天免费送10000份咖啡&#xff0c;我是个不喝咖啡的人儿&#xff0c;所以没咋关注&#xff0c;今早我们的团宠小妹&#xff0c;拉着我 10点整拼手速&#xff0c;想着帮她抢一杯&#xff0c;于是点开瑞幸咖啡小程序主页&#xff0c;banner 栏…

数学建模常用算法之主成分分析

数学建模常用算法之主成分分析 引言步骤实例以及代码 引言 主成分分析是一种降维算法&#xff0c;它能将多个指标转换为少数几个主成分&#xff0c;这些主成分是原始变量的线性组合&#xff0c;且彼此之间互不相关&#xff0c;且能反映出原始数据的大部分信息。 一般来说&#…

机器学习笔记 - vision transformer(ViT)简述

一、ViT简述 视觉转换器 vision_transformer(ViT) 因其令人印象深刻的准确率和计算效率而迅速成为卷积神经网络 (CNN) 在计算机视觉任务中的首选替代品。ViT模型在许多数据集和任务组合中的表现比CNN高出近4倍,从而确立了自己作为非常强大的竞争者的地位。 同样,基于转换…

采样中断服务程序原理

采样中断服务程序框图如图2&#xff0d;6所示。采样中断服务程序主要包括采样计算&#xff0c;TV、TA断线自检和保护起动元件三个部分。同时还可以根据不同的保护特点&#xff0c;增加一些检测被保护系统状态的程序。 一、采样计算概述 进入采样中断服务程序&#xff0c;首先进…

抖音林客服务商入驻申请

抖音林客服务商后台提供了以下主要功能&#xff1a; 数据报告&#xff1a;可以查看账户的广告投放、效果等数据报告&#xff0c;并进行数据分析和优化。 广告投放平台&#xff1a;可以创建和管理广告投放计划、定向和出价等设置&#xff0c;以及监控广告投放效果。 …

【032】C++高级开发之继承机制详解(最全讲解)

C的继承机制详解 引言一、继承和派生1.1、继承的概念和意义1.2、派生类的定义 二、继承中的构造和析构2.1、子类的构造和析构顺序2.2、子类调用成员对象、父类的有参构造 三、子类和父类的同名处理3.1、子类和父类同名成员数据3.2、子类和父类同名成员函数3.3、子类重定义父类的…

2.5C++多重继承

C 多重继承概述 C中的多重继承是指一个派生类可以从多个基类中继承属性和方法。 多重继承的作用是让 C 中的类更灵活地组合&#xff0c;以及实现代码的高复用。 多重继承的语法如下&#xff1a; access_specifier可以是public、protected或private&#xff0c;用来指定继承…