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. Android为TV端助力之点击Textview无效

    记录一下如果有两个Textview都有点击事件,那么不能给Textview同时设置 android:focusable="true"android:focusableInTouch ...

  2. 红米手机使用应用沙盒一键修改sdk信息

    前面文章介绍了怎么在安卓手机上安装激活XPOSED框架,XPOSED框架的极强的功能各位都介绍过,能不修改APK的前提下,修改系统内核的参数,打个比方在某些应用情景,各位需要修改手机的某个系统参数,这 ...

  3. [LeetCode] 198. 打家劫舍 ☆(动态规划)

    描述 你是一个专业的小偷,计划偷窃沿街的房屋.每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警. 给定一个 ...

  4. 美好的童年伙伴:360 智能儿童手表 P1体验评测

    写在前面 少年儿童作为祖国的花朵,未来的栋梁,也是我们每个做家长的心头肉.近年来各种新闻报道中校园欺凌.虐待事件频发,虽然依然只是个别事件,但我们依然会心怀担忧. 360作为安防软件起家的专业公司,凭 ...

  5. ​Python 3 新特性:类型注解——类似注释吧,反正解释器又不做校验

    ​Python 3 新特性:类型注解 Crossin ​ 上海交通大学 计算机应用技术硕士 95 人赞同了该文章 前几天有同学问到,这个写法是什么意思: def add(x:int, y:int) - ...

  6. 下载恶意pcap包的网站

    说几个我经常用的,免费的:1.  Malware  Traffic  Analysis:  http://www.malware-traffic-analysis.net/2018/index.htm ...

  7. asp.net 访问局域网共享文件

    最近有个项目ASP.NET的项目,要读写一个局域网里的共享文件夹上的文件,记录如下: 1.访问共享文件 在这里我定义了一个方法,SaveFileExist(filesrc,filename),这个方法 ...

  8. python开发笔记-类

    类的基本概念: 问题空间:问题空间是问题解决者对一个问题所达到的全部认识状态,它是由问题解决者利用问题所包含的信息和已贮存的信息主动的地构成的. 初始状态:一开始时的不完全的信息或令人不满意的状况: ...

  9. Laravel —— 路由问题

    在 Laravel 中,路由是项目的起点. 下面总结一些路由中常见的问题. 一.路由 404 是因为配置文件没有开启重定向模块,可以通过下面的操作解决. 1.php.ini 开启 openssl 模块 ...

  10. lambda表达式格式以及应用场景?

    lambda表达式,通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数. add = lambda x, y : x+y print(add(1,2)) # 结果为3 应 ...