在微服务架构下高覆盖率的单元测试是保障代码质量的第一道也是最重要的关口,应该锲而不舍。java
单元测试为代码质量保驾护航,是提升业务质量的最直接手段,实践证实,很是多的缺陷彻底能够经过单元测试来发现,测试金字塔提出者Martin Fowler 强调若是一个高层测试失败了,不只仅代表功能代码中存在bug,还意味着单元测试的欠缺。所以,不管什么时候修复失败的端到端测试,都应该同时添加相应的单元测试。 而越早发现发现Bug,形成的浪费就会越小,单元测试自己就可以提供了快速反馈的机制。另外,单元测试是一个优秀的开发工程师必备技能之一,优秀的单元测试是业务快速投产的加速器。spring
虽然对于100%的单元测试覆盖率咱们持有保留态度,但在一个微服务架构基础设施还不完善、开发人员能力良莠不齐、DDD(领域驱动设计)能力不足以应对复杂业务的状况下,单元测试是性价比最高的实践。单元测试能够充当一个设计工具,它有助于开发人员去思考代码结构的设计,让代码更加有利于测试,知足架构的可测性设计要求。数组
单元测试的意义包括以下内容:网络
尽早发现缺陷,下降开发投入成本架构
85%的缺陷是代码阶段产生的,单元测试阶段能够发现绝大部分软件缺陷。同时软件产品的缺陷发现的越早每每会大大的下降其开发的投入成本,其缺陷的发现时间与修复缺陷的成本以下图中红色曲线。红色曲线代表随着软件开发的进行,漏洞越早发现,其修复的成本越低,而且其修复成本与开发进度的上升趋势越在后期越接近于指数上升。框架
不管是对单体项目仍是单体项目向微服务架构迁移,代码都在不断的在变化和重构,经过单元测试,开发能够放心的修改重构代码,减小改代码时心理负担,提升重构的成功率。编辑器
越是良好设计的代码,越容易编写单元测试,多个小的方法的单测通常比大方法(成百上千行代码)的单测代码要简单、要稳定,一个依赖接口的类通常比依赖具体实现的类容易测试,因此在编写单测的过程当中,若是发现单测代码很是难写,通常代表被测试的代码包含了太多的依赖或职责,须要反思代码的合理性,进而推动代码设计的优化,造成正向循环。
选择测试驱动开发(TDD)的模式进行项目开发,以单元测试引导项目实现。这种模式下单元测试先行,根据单元测试代码开发功能代码,进而很是精准的实现业务需求,减小返工和缺陷率,可提升项目质量和效率。函数
“spring-boot
单元测试浪费了太多的时间微服务
虽然不进行单元测试能够更快的交付到后续测试阶段,可是在后续集成测试阶段、系统测试阶段会发现更多的缺陷甚至软件没法运行的致命缺陷,这些缺陷修复的时间远超过单元测试的时间。另外没有单元测试的代码后期软件进行重构或者改进时花费的时间也比有单元测试的所花费的时间要多不少。因此说完整计划下的单元测试是对时间的更高效的利用。
已经有接口集成测试、系统功能测试进行质量保证了,集成测试阶段对接口进行全面测试就能够达到单元测试的要求,不必作重复工做在进行单元测试。
接口测试和功能测试没法覆盖全部的代码,这样若是缺陷存在则将被遗漏,而且Bug将被带到生产上去。一旦用户使用过程当中触发了这些没有测试的代码就会带来严重的经济后果。
跑通一个业务主流程等价于作过单元测试
目前有不少开发人员认为,开发完代码以后,写个main方法,从入口调完全部的模块,最后验证下返回结果,就认为作过单元测试了,这种想法是及其错误的,这充其量算一种不全面的冒烟测试,是对单元测试概念的错误认知。
下面将从单元测试所处的阶段、单元测试用例设计规范、单元测试实现几个维度分别介绍如何在微服务架构下开展单元测试。 首先看下单元测试所处的阶段,下图为非TDD模式下单元测试所处的阶段
由图可见单元测试处在特性分支开发完成以后,具体的描述以下:
1.开发人员从Master分支拉取特性分支做为开发分支;
2.开发完特性分支后、代码构建、单元测试、静态代码扫描;
3.经过后合并到Master分支,用于投产。
下面看下什么样的单元测试用例是优秀的用例,是即知足运行速度又知足高覆盖率的用例。随行付定制了单元测试规范,下面节选了强制要求的部分规范。优秀的单元测试用例要符合如下用例设计规范的要求。
1.必须遵照 AIR 原则
【说明】单元测试在线上运行时,感受像空气(AIR)同样并不存在,但在测试质量的保障上,倒是很是关键的。好的单元测试宏观上来讲,具备自动化、独立性、可重复执行的特色。 A:Automatic(自动化) I:Independent(独立性) R:Repeatable(可重复)
2.单元测试应该是全自动执行的,而且非交互式的
【说明】测试框架一般是按期执行的,执行过程必须彻底自动化才有意义。输出结果须要人工检查的测试不是一个好的单元测试。单元测试中不许使用 System.out 来进行人肉验证,必须使用 assert 来验证。
3.保持单元测试的独立性
【说明】为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的前后次序。反例:method2 须要依赖 method1 的执行,将执行结果作为 method2 的输入
4.单元测试是能够重复执行的,不能受到外界环境的影响
【说明】单元测试一般会被放到持续集成中,每次有代码 check in时单元测试都会被执行。若是单测对外部环境(网络、服务、中间件等)有依赖,容易致使持续集成机制的不可用。
5.对于单元测试,要保证测试粒度足够小,有助于精肯定位问题。单测粒度至可能是类级别,通常是方法级别
【说明】只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试的领域
6.核心业务、核心应用、核心模块的增量代码确保单元测试经过
【说明】新增代码及时补充单元测试,若是新增代码影响了原有单元测试,请及时修正
7.单元测试代码必须写在以下工程目录:src/test/java,不容许写在业务代码目录下
【说明】源码构建时会跳过此目录,而单元测试框架默认是扫描此目录
随行付在推行单元测试落地过程当中采用按部就班的方式,逐步增长单元测试用例达到单元测试规范中规定的覆盖率要求。须要说明的是咱们不是追求覆盖率这个数字指标,那样就舍本求末了,咱们是经过覆盖率这个能够量化的指标实现提升代码质量的这个根本目的。
第一阶段:单元测试覆盖率要求至少25%
第二阶段:单元测试覆盖率要求至少60%
第三阶段:单元测试覆盖率要求至少80%
随行付单元测试覆盖率统计一样采用SonarQube平台结合Jenkins工具,Jacoco单元测试覆盖率工具完成,这个同上篇介绍的静态代码扫描流程是一脉相承的。同时要求开发人员本地的IDE工具中安装Jacoco覆盖率插件,当本地开发完单元测试用例并构建后,便可看到覆盖率信息,进而能够快速补充用例,达到覆盖率要求。 以Eclipse为例,当开发完单元测试代码后,按照以下操做便可查看覆盖率信息。
1.选择须要统计的java测试代码或者包;
2.右键,Coverage as->Junit
3.覆盖率结果会自动在Coverage 视图中展现出来;
4.在Java编辑器中用不一样的颜色标识代码的覆盖状况。
【说明】 绿色----全覆盖 红色----未覆盖 黄色----部分覆盖
下面介绍下在微服务下应该如何进行单元测试。为了有效的进行单元测试,须要遵循必定的方法,一般采用路径覆盖法设计单元测试用例。所谓路径覆盖法就是选取足够多的测试数据,使程序的每条可能路径都至少执行一次(若是程序图中有环,则要求每一个环至少通过一次)。具体设计过程参见以下步骤:
1.画出程序控制流程图
2.计算圈复杂度
3.找出全部程序基本路径
4.根据路径设计测试数据
如下图代码为例说明路径覆盖法的设计单元测试的过程
1.首先根据代码画出其对应的流程图以下,图中数字表明行号。当条件语句中包含多个条件时应予以拆分,如第13行,拆分为13.1和13.2;对于没有分支和循环的语句可忽略,如第16行。
有了流程图后,咱们能够根据它计算出圈复杂度,这个能够做为测试用例数的上限,圈复杂度计算公式以下:
V(G)= E - N + 2,E是流图中边的数量,N是流图中结点的数量。 V(G)= P + 1 ,P是流图G中断定结点的数量。
两个公式用哪一个都行,最后的结果应该是同样的。这里咱们用第二个公式,V(G)= 3 + 1 = 4,也就是咱们只须要设计4条用例便可覆盖全部路径
接下来就是找出全部基本路径,基本路径是从程序的开始结点到结束能够选择任何的路径遍历,可是每条路径至少应该包含一条已定义路径未曾用到的边,全部的基本路径以下
A
B C
B D E F
B D E G E F
获得了全部的基本路径,剩下的简单了,只须要按照路径设计出对应的入参数据便可
案例1:a = 0, b = 1, 指望值 -1
案例2:a = 1, b = 0, 指望值 -1
案例3: a = 4, b = 2, 指望值 2
案例4:a = 8, b = 12, 指望值 4
除此以外,单元测试用例设计还须要考虑如下场景
边界值
业务边界 溢出边界 字符串、数组、集合等的边界
异常场景
业务异常 输入异常(如参数不合法)
正常场景
单个模块的用例设计均可以按照路径覆盖法达到语句覆盖和分支覆盖,可是对于有依赖关系的模块
在微服务架构下,每一个模块之间会存在依赖的状况,为了保持单元测试的独立性原则,在不依赖于外部条件的状况下制造各类输入数据,须要借助Mock技术,其本质是用一个模拟的对象代替真实的对象(例如一个类、模块、函数或者微服务)。模拟对象的行为特征和真实对象很是类似,采用相同的调用逻辑,返回内容按照以前预约义的内容返回,提供返回数据。Mock技术的原理能够用以下案例进行解释。
当要进行单元测试时,须要给A注入B和C,可是C又依赖D,D又依赖E。这就致使了,A的单元测试不知足独立性原则。 但使用了Mock来进行模拟对象后,就能够把这种依赖解耦,只关心A自己的测试,它所依赖的B和C,所有使用Mock出来的对象,而且给MockB和MockC指定一个明确的行为。
在单元测试工具的选择方面,随行付单元测试借助Junit工具和Mockito工具进行单元测试,微服务架构下无论是spring boot仍是spring cloud,一般使用@SpringBootTest注解进行单元测试。一个单元测试的实现步骤主要包括4步:
1.设置测试数据
2.Mock依赖的系统并给定预期值,若是没有依赖这步能够省略
3.在测试中调用方法
4.断言返回的结果是否符合预期
下面以一个很是简单的例子介绍在微服务架构下如何对spring boot中的controller层和service层进行单元测试。
调用逻辑简化版如图所示,Controller调用ServiceA,ServiceA依赖ServiceB。
被依赖ServiceB的代码以下
package cn.vbill.quality.service;
import org.springframework...Service;
@Service
public class ServiceB {
public boolean serve(int param) { return param % 2 == 0; }
}
被测ServiceA的代码以下
package cn.vbill.quality.service;
import org.springframework.beans...Autowired;
import org.springframework.stereotype.Service;
@Service
public class ServiceA {
@Autowired
private ServiceB srvB;
public String doSomething(int param) {
if (srvB.serve(param)) { return "even"; } return "obb";
}
}
ServiceA和ServiceB的逻辑很是简单,如今测试ServiceA,步骤以下:
首先:在gradle中增长测试须要的依赖包
// 可根据实际状况添加版本号
testCompile("org.springframework.boot:spring-boot-starter-test")
其次:在src/test/java下面建立测试类,采用@SpringBootTest注解和Mockito技术对ServiceB进行测试和Mock,更多Mockito的使用能够参考其余文章,这里不过多介绍。代码以下:
最后,使用覆盖率工具查看单元测试覆盖率,以下图所示,实现了100%覆盖。
ServiceB没有任何依赖,所以对它测试就按照常规的Junit测试便可,这里不过多介绍。下面介绍Controller层的单元测试,总体上看 Controller 层的测试和 Service 层大体相同,只不过是咱们不去直接调用 Controller 的方法,而是经过MockMvc模拟HTTP请求。从逻辑图上看Controller是直接调用ServiceA,所以须要使用Mockito模拟ServiceA。
被测Controller代码逻辑以下:
测试类以下
最后,经过覆盖率工具查看单元测试覆盖率为100%,作到了全覆盖。
以上是如何在微服务架构下进行单元测试进行了详细的介绍,在微服务架构下高覆盖率的单元测试是保障代码质量的第一道也是最重要的关口,应该锲而不舍。
本篇分别从微服务架构下开展单元测试的意义、对单元测试的常见误解以及如何开展单元测试三个方面进行介绍,单元测试是一项成本低、收益高的实践,要利用好这把利剑,打好代码质量基础,为后续的质量保证过程添砖加瓦。
王田,随行付架构部测试架构师。负责测试方法论布道、自动化测试工具研究与推广。