Springboot单元测试Junit深度实践

前言

单元测试的好处估计大家也都知道了,但是大家可以发现在国内IT公司中真正推行单测的很少很少,一些大厂大部分也只是在核心产品推广单测来保障质量,今天这篇文章就是介绍下单测的方法论和如何在Springboot中解决类之间的依赖来实施junit单元测试。

先来他轮下大家不做单元测试的原因:

  1. 产品经理天天催进度,哪有时间写UT。
  2. UT是测试自己的代码,自测?那要QA何用?
  3. 自测能测出bug?都是基于自身思维,就像考试做完第一遍,第二遍检查一样,基本检查不出什么东西。
  4. UT维护成本太高,投入产出比太低
  5. 不会写UT

只有真正尝到UT的好处的甜头才会意识到UT的价值。

其实这篇文章大部分章节是讲述单元测试的难点和解决办法,springboot如何集成其实很简单文章中会讲到。

单元测试的困难

假设我们一个service实现依赖某个RPC Service那我们要测试的这个类的话需要做哪些工作。

第一步:数据准备

跑到别人家的数据库插几条数据?或者跟PRC Service的Owner商量好,搭一个测试环境供我们测试?有些公司还真有专门的自动化测试环境,那么即使有测试环境,那如何实现各种case场景下,第三方Service很配合的返回数据给我们?想想都蛋疼。

第二步:执行方法

假设我们成功的解决了第一步中的问题,皆大欢喜。现在来看第二步,假设我们的service里面调用了另一个RPC Service创建了很多数据,跑了无数次case,结果….RPC Service对应的数据库都是我们的脏数据,如何清理?而且他们敢随便删数据吗?想想也蛋疼。

第三步:输出验证

假设我们又愉快的解决了第二步中的问题。现在来看第三步,假设我们的方法执行最终输出是创建了一个订单,订单当然是调用订单Service接口了,那么我们如何验证订单是否成功创建了呢?或许可以调用订单Service查询订单的接口来验证。很明显大多数情况下并没有这么完美。想想也蛋疼呀。

通过以上分析,Local Integration Test是可行的,Remote Integration Test基本不可行。

Mock解决单测问题

对各个模块的依赖可能是我们做单元测试过程中遇到最讨厌的问题,从我单元测试经验来看,解决这类问题一般有两个方法:

1)建立挡板环境,对于外部依赖的系统接口都建立挡板。因为可能依赖的系统接口很多并且建立挡板环境会浪费很多资源,所以对于单元测试而言这样其实还是会依赖于挡板环境,每次切挡板也是一个痛苦的过程,修改挡板数据也会比较麻烦 ,所以我们一般不建议采用挡板来进行单元测试。

2)建立Mock类,对被测试的每个类中依赖的类都建立mock,并且对mock类用到的方法要写桩和桩数据,这个听起来感觉工作量很大,写mock确实是单元测试过程中工作量最大的地方,但是一旦mock写好以后,我会会发现被测类可以独立于任何模块,可以和一切解耦,当你写单元测试过程中发现写的mock很多的时候,这就说明我们这个类外部依赖太多,可以思考着么设计这个类是不是有问题,有没有更好的设计松耦合的方案。所以为什么很多公司推崇TDD就是这个原因,TDD让前期的设计环节考虑的更多,让类和模块的设计更合理。

Springboot+Junit+Mockito

Mockito目前已经被集成到了springboot-test包中,只需要在工程的pom文件中引入spring-boot-starter-test就可以了,其中包括了junit和mockito类库。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

写junit本身是比较简单的,最复杂的地方就在于写mock类和对应的桩,网上的大部分例子都是比较简单的在springboot中集成junit后简单的assert,并且被测类都是没有任何依赖的简单功能类,但是我们在实际开发过程中不可能都是这样的没有依赖的简单模块,这里笔者详细讲述下我以前遇到的比较麻烦的问题,相信也是刚开始写单元测试的朋友会遇到的两个问题,一个是怎么mock Spring启动自动注入的bean,还有一个比较麻烦的就是如何给mock类写桩。

解决@Autowired对象的mock

首先我们说明下场景,笔者比较懒直接拿最近写的代码来举例子说明。

APMInfoServiceImpl是我们的被测业务类,这个Service类具体业务逻辑大家不用关心,大家只要知道这个被测类中有一个spring容器自动注入的APMInfoMapper的对象实例就可以了,我们下面就要对APMInfoServiceImpl中的APMInfoMapper实例做mock,并注入到APMInfoServiceImpl中。

@Service
public class APMInfoServiceImpl implements APMInfoService {
@Autowired
private APMInfoMapper apmInfoMapper; @Override
public List<HotAppTimeEntity> queryHotAppTime(int limit){
...
return hotAppTimeEntityList;
} @Override
public List<HotInterfaceTimeEntity> queryHotInterfaceTime(int limit) {
...
return hotInterfaceTimeEntityList;
}
}

下面就是我们的测试类了,首先在测试类名前面需要加上  @RunWith(SpringRunner.class),这句表示该测试类运行的时候会先加载spring框架所需的相关类库并将所有有注解的类进行自动依赖注入。

在测试类中,我们需要在被测类对象声明的时候加上@InjectMocks,这个注解从名字也很好理解,就是将所有的mock类注入到这个对象实例中,注意这里对APMInfoService的创建必须要通过new来初始化,不能像@Autowired那样靠spring自动注入依赖类,因为这里APMInfoService内部依赖的类都是Mock的对象,必须要显式创建类实例Mockito才能注入成功。这样你就会发现在下面测试方法调用的时候被测类就不会再是null了。

@RunWith(SpringRunner.class)
public class APMInfoServiceImplTest {
@InjectMocks
private APMInfoService apmInfoService = new APMInfoServiceImpl();
@Mock
private APMInfoMapper apmInfoMapper; @Before
public void setUpHotAppData() {
//准备桩数据,queryHotAppTime mock normal data
List<HotAppTimeEntity> hotAppTimeEntityList = new ArrayList<>();
HotAppTimeEntity hopAppTimeEntity1 = new HotAppTimeEntity();
//省略一堆set方法调用。。。
HotAppTimeEntity hopAppTimeEntity2 = new HotAppTimeEntity();
//省略一堆set方法调用。。。
hotAppTimeEntityList.add(hopAppTimeEntity1);
hotAppTimeEntityList.add(hopAppTimeEntity2);
when(apmInfoMapper.queryHotAppTime(5, DateUtil.today())).thenReturn(hotAppTimeEntityList); HotAppTimeEntity hopAppTotal = new HotAppTimeEntity();
hopAppTotal.setTotalNum(new Long(100));
//写桩方法
when(apmInfoMapper.queryHotTotal(DateUtil.today())).thenReturn(hopAppTotal); //queryHotAppTime mock null data
when(apmInfoMapper.queryHotAppTime(4, DateUtil.today())).thenReturn(null);
} @Test
public void queryHotAppTime() throws Exception {
//normal data,正常数据
List<HotAppTimeEntity> hotAppTimeEntityList = apmInfoService.queryHotAppTime(5);
Assert.assertEquals("10001", hotAppTimeEntityList.get(0).getAppID());
Assert.assertEquals(6.0, hotAppTimeEntityList.get(0).getAvgDuration(), 0.0000);
Assert.assertEquals(0.8, hotAppTimeEntityList.get(1).getSuccessRate(), 0.0000);
Assert.assertEquals(0.1, hotAppTimeEntityList.get(1).getRatio(), 0.0000); //null data,null数据处理
List<HotAppTimeEntity> hotAppTimeEntityNullList = apmInfoService.queryHotAppTime(4);
Assert.assertNull(hotAppTimeEntityNullList);
}

为mock类依赖方法写桩

被测类和其中依赖的类我们已经通过Mockito创建好了,那么这样是不是就可以测试了?当然不是因为依赖的APMInfoMapper对象都是我们Mock出来的,都是假的,要让测试能正常运行起来,我们还需要给APMInfoMapper被调用到的方法写桩,桩很好理解,就是我们常说的挡板,当方法的输入A返回B。Mockito提供的桩方法也很简单就是when(A).thenReturn(B)这样的结构,when有很多种重载方法,具体如何使用建议参考Mockito的接口文档,官方是英文的,也可以看网友翻译的中文版 https://blog.csdn.net/bboyfeiyu/article/details/52127551

从这个例子中我们可以看到我给apmInfoMapper写了queryHotAppTime()和queryHotTotal()两个桩方法,为什么是这两个,很好理解,因为我在APMInfoServiceImpl中用到了这两个方法,这样当我在测试类中调用APMInfoServiceImpl的queryHotAppTime方法时,方法内部使用了apmInfoMapper的queryHotAppTime()和queryHotTotal()的地方就会返回我设置的两个桩方法,并return我提前设置好的返回内容,这样APMInfoServiceImpl的queryHotAppTime方法就不会报错了,并且我可以根据方法实现内部的逻辑设计不同的桩方法返回来覆盖到所有的逻辑分支,所有都在自己的掌控之中,最关键的是这个测试方法不再依赖其他任何一个类,唯一以来的类也自己实现了桩方法,并且大家可以发现不用把springboot的应用容器运行起来,所以测试速度非常快。

总结

看到这里大家是不是对单元测试有了深入的了解,并且也能够知道如何解除对外部的依赖,那么就让我们开始把单测写起来吧。这时候肯定有人会说,如果所有类都这样写单元测试,那么工作量太大了,而且单元测试代码量比实际业务代码量还要大,甚至大好几倍。这个确实是这样的,测试代码一般是业务代码的2-3倍,是不是所有类所有方法都要测到呢?

这个问题也是比较经典的,一个方法要是所有的路径都覆盖到,那么要写很多的case,工作量绝对会死人,而且项目经理和产品经理也不会同意开发人员把时间都花在测试代码上。我的建议是两个原则:

  1. 核心逻辑,容易出错的逻辑一定要覆盖到。
  2. 根据自己的时间。 没必要写的非常多,毕竟case维护成本很高,业务逻辑一改,case得跟着改。

当开发完功能,跑完UT全部绿灯,那一刻是作为一个开发人员最爽的,因为你可以从你的立场安全上线你的代码了,这是一件非常有成就感非常自豪的事情,记得以前每次提交代码都会担心测试测出bug、上线出现问题等等,天天心惊胆战的,写了单测以后真的可以让开发人员放心去休息了,希望大家也能达到这种状态,这个状态非常美好:)

原文地址https://www.jianshu.com/p/afb04b925db3

Springboot单元测试Junit深度实践的更多相关文章

  1. SpringBoot 单元测试junit test

    pom引用 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http: ...

  2. Springboot的日志管理&Springboot整合Junit测试&Springboot中AOP的使用

    ==============Springboot的日志管理============= springboot无需引入日志的包,springboot默认已经依赖了slf4j.logback.log4j等日 ...

  3. springmvc,springboot单元测试配置

    1. springmvc单元测试配置 <dependency> <groupId>junit</groupId> <artifactId>junit&l ...

  4. Zabbix监控系统深度实践

    Zabbix监控系统深度实践(企业级分布式系统自动化运维必选利器,大规模Zabbix集群实战经验技巧总结,由浅入深全面讲解配置.设计.案例和内部原理) 姚仁捷 著  ISBN 978-7-121-24 ...

  5. 深度实践KVM笔记

    深度实践KVM笔记 libvirt(virt-install,API,服务,virsh)->qemu(qemu-kvm进程,qemu-img)->KVM虚拟机->kvm.ko 内核模 ...

  6. Spring注解AOP及单元测试junit(6)

    2019-03-10/20:19:56 演示:将xml配置方式改为注解方式 静态以及动态代理推荐博客:https://blog.csdn.net/javazejian/article/details/ ...

  7. TiDB 深度实践之旅--真实“踩坑”经历

    美团点评 TiDB 深度实践之旅(9000 字长文 / 真实“踩坑”经历) 4   PingCAP · 154 天前 · 3956 次点击 这是一个创建于 154 天前的主题,其中的信息可能已经有所发 ...

  8. [5.19 线下活动]Docker Meetup杭州站—拥抱Kubernetes,容器深度实践

    对本次线下活动感兴趣的朋友,欢迎点击此处报名,领取免费票. 今年3月,Docker刚刚过完5岁生日,五年期间,Docker也逐渐在技术和实践方面趋于成熟,更是在去年年底主动拥抱Kubernetes. ...

  9. 2017-09-26 发布 SpringBoot多模块项目实践(Multi-Module)

    https://segmentfault.com/a/1190000011367492?utm_source=tag-newest 2017-09-26 发布 SpringBoot多模块项目实践(Mu ...

随机推荐

  1. 不依赖Python第三方库实现梯度下降

    认识 梯度的本意是一个向量(矢量),表示某一函数在该点处的方向导数沿着该方向取得最大值,即函数在该点处沿着该方向(此梯度的方向)变化最快,变化率最大(为该梯度的模), 我感觉, 其实就是偏导数向量方向 ...

  2. git指令集合

    原网页:https://www.linuxidc.com/Linux/2018-04/151809.htm Git 是一个很强大的分布式版本控制系统.它不但适用于管理大型开源软件的源代码,管理私人的文 ...

  3. 8 个 Python 实用脚本,赶紧收藏备用!

    脚本写的好,下班下得早!程序员的日常工作除了编写程序代码,还不可避免地需要处理相关的测试和验证工作. 例如,访问某个网站一直不通,需要确定此地址是否可访问,服务器返回什么,进而确定问题在于什么.完成这 ...

  4. Linux命令——jobs、bg、fg、nohup

    参考:Bash基础——工作管理(Job control) jobs -l :除了列出 job number 与命令串之外,同时列出 PID 的号码: -r :仅列出正在背景 run 的工作:-s :仅 ...

  5. SHELL脚本编程-普通数组(列表)和关联数组(字典)

    SHELL脚本编程-普通数组(列表)和关联数组(字典) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.数组相关概述 变量: 存储单个元素的内存空间 数组: 存储多个元素的连续的 ...

  6. MySQL/MariaDB数据库的复制监控和维护

      MySQL/MariaDB数据库的复制监控和维护 作者:尹正杰  版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.清理日志 1>.删除指定日志文件名称之前的日志(也可用基于时间) M ...

  7. 使用 ESlint、lint-staged 半自动提升项目代码质量

    最近在项目部署了ESlint还有一些配套的工具,比如 prettier husky lint-staged,有些心得写出来分享下. 依据本篇可以实现在git commit之时,重新格式化代码,同时进行 ...

  8. 使用Xpath爬虫库下载诗词名句网的史书典籍类所有文章。

    # 需要的库 from lxml import etree import requests # 请求头 headers = { 'User-Agent': 'Mozilla/5.0 (Windows ...

  9. Java线程状态、线程start方法源码、多线程、Java线程池、如何停止一个线程

    下面将依次介绍: 1. 线程状态.Java线程状态和线程池状态 2. start方法源码 3. 什么是线程池? 4. 线程池的工作原理和使用线程池的好处 5. ThreadPoolExecutor中的 ...

  10. Codeforces Round #555 (Div. 3) F. Maximum Balanced Circle

    F. Maximum Balanced Circle 题目链接 题意 给出\(n\)个数,现在要从中选出最多的数\(b_i,b_{i+1},\cdots,b_k\),将这些数连成一个环,要求两两相邻的 ...