如何将bug杀死在摇篮里?
阿里妹导读:在欧洲中世纪的传说中,有一种叫“人狼”的妖怪,就是人面狼身。它们会讲人话,专在月圆之夜去袭击人类。而且传说中对“人狼”用一般的枪弹是不起作用的,普通子弹都伤不到也打不死它,只有一种用银子作成的特殊子弹才能把它杀死。Brooks在他最著名的随笔文章《No Silver Bullet》里引用了这个典故 ,说明在软件开发过程里是没有万能的终杀性武器的,只有各种方法综合运用,才是解决之道。
那么在软件研发过程中,哪怕没有银弹,如何用各种方法去解决这些“人狼”带来的威胁呢?阿里巴巴在多年的研发过程中,又是如何对付这头“人狼”?这一路走来,又有哪些方法和实践沉淀?
1.阿里巴巴研发协同平台介绍
阿里巴巴研发协同平台(AONE)是阿里一站式智能研发协同平台,目前为阿里巴巴集团,下属子公司以及生态合作伙伴提供从“需求->编码->测试->发布->反馈”端到端的持续交付服务,并解决研发过程中跨角色、跨组织、跨地区的协作问题,在此基础上通过数据驱动度量分析为组织效能提升提供决策依据,目前这一平台已经上云对外提供服务,称为阿里云研发协同RDC。(以下简称为RDC)
上图是整个RDC的业务框架,RDC采用微服务的架构,从规划到运营提供了业务全生命周期的服务,众多的服务造成了RDC的复杂度,另一方面作为一个面向用户的平台,RDC又涵盖了阿里三个层次的技术服务栈。
第一层,基础资源层,包括idc机房、网络设施、OS、Docker、数据库等。
第二层,平台服务层,包括中间件、调度层、应用服务器等。
第三层,RDC研发协同平台,包括 项目、代码、应用、测试、交付、运营的管理。
三个层次加起来涉及的核心应用达50+,随便一个风吹草动就会对系统的稳定性造成影响,这些影响最终体现到面向用户的RDC,因此好的质量保障才能为用户提供一个稳定,高可用性的研发协同平台。
2.质量保障策略
总体采用 “事前预防”,“事中控制”,“事后总结改进”的思想,主要做的三件事情:
-
测试驱动持续交付
测试驱动持续交付,每一次的持续集成和发布都从单元测试,API测试,集成测试,UI测试进行自动化测试覆盖;具体包括:单元测试,集成测试,WebUI测试,移动端UI测试,压测,线上引流的API测试,线上冒烟测试。 -
线上质量监控
对线上质量的保障,主要采用监控的方式,希望一有问题就能及时发现解决,避免问题的大面积扩大。主要包括:
(1) 机器监控报警,业务数据监控。
(2) 对线上运行日志的聚合分析,发现存在的错误。
(3) SLA数据质量提升,将业务数据可视化,指导改进方向。
(4) 面向业务的监控和故障演练,通过对线上7*24的监控提前发现问题保障系统的稳定运行。 -
研发质量提升
通过代码审核,集团规约扫描,规范代码和提升研发质量。
3. 持续交付流程
上图是测试驱动的的持续交付流程,也是RDC各个应用在发布时的持续交付流程,通过RDC的发布功能实现。
(1) 开发在变更(Change Request)中提交代码后,触发单元测试,检查代码中基本的逻辑问题,实践中单测的覆盖率和维护由开发同学自己保证,目前RDC核心应用的单测行覆盖率在50%左右。
(2) 部署测试环境,开发进行自测,主要对新的功能进行基本的验证。测试环境与生产环境完全隔离,具有独立的网络环境,单独的数据库和中间件等依赖环境。
(3) 自测完成后发布预发环境,由测试同学进行集成测试,包括回归测试和新功能的验证,以及对核心接口的小压测。预发环境与生产环境在同一个网络中,共享同一个数据库,数据隔离采用逻辑隔离,但是它具有一个独立的依赖环境,如独立的上下游和中间件。
(4) 接着进入Beta测试环节,通过截取一部分生产环境的流量到Beta环境回放来对核心接口做进一步的验证。Beta环境与线上环境完全相同,不同的是Beta环境不对外提供服务。
(5) Beta测试完成后,正式发布生产环境并对生产环境执行冒烟测试。
以上是一个变更的生命周期,也是一次完整的持续交付流程。得益于RDC发布系统和RDC实验室的集成,我们可以在持续交付的流程中实现测试的卡点,测试不通过,无法进入下一个阶段的发布。
4.测试技术和方法
以下内容重点讨论测试方法和实现方案,以及如何通过RDC完成持续交付,不涉及代码实现,部分服务也会在未来上云提供给外部用户试用。
4.1 分层的自动化测试
传统的自动化测试更关注的产品UI层的自动化测试,而分层的自动化测试倡导产品的不同阶段(层次)都需要自动化测试。在《google 测试之道》一书,对于google产品,70%的投入为单元测试,20%为接口、集成测试,10% 为UI层的自动化测试。如下图的金字塔形,越往上的投入越低,按照代码统计的测试覆盖率也越低。
4.2 测试框架
-
测试框架的选型
在开始RDC的自动化前团队对其他测试工具有了一定的积累,各种流行工具,数据驱动,关键字驱动工具,如Fitness,RobotFramework等,也调研过一些特定的测试工具如对http测试的Jmeter, Postman, SoapUI等。
这些工具普遍存在的一个问题是不够灵活,工具本身比较重,调试麻烦;另一些比较轻量的工具又功能单一,针对性强,无法作为一个测试框架使用,测试场景比较简单时非常好用,一旦场景变复杂,就带来了大量的维护工作。
其次也开发过一些看起来很高大上的框架,有前端有后端,目的是减少测试人员的coding工作,一键完成测试,看起来很高大上。 但实际的问题是测试人员的主要工作变成测试框架的维护,最后的结果是业务绑定太强,无法推广,而自己的用例覆盖率也没有上去。
最后选择了TestNG作为主测试框架,其他的功能全部交给RDC完成。选择TestNG的理由:
(1)是灵活,作为程序员可以用代码实现灵活的场景组织和功能,只要稍微二次开发一下。
(2)是跟很多框架一样有Before,after,listener的概念,基于单元测试却比junit强大,对场景的准备,还原,清理都能较好的处理。
(3)使用java,也许不是好的测试语言,但是团队的技术储备java是强项,拥有丰富的开源测试库,能够完成单测,API,集成测试,WebUI,移动UI的测试;另一个原因是RDC本身也采用Java开发。
(4)缺点是冗余,作为一个测试框架,要实现测试比其他的测试框架要多一些代码。但是作为一个需要长期维护的测试工程,这些冗余也还可以接受。
-
基于TestNG的测试框架
采用分层的方式
-
如图所示,整个测试框架分为四层,通过分层的方式,测试代码更容易理解,维护起来较为方便。
第一层是基础测试库:包括selenium,macaca,ssh库,httpclient,cli库等。
第二层是服务层,相当于对测试对象的一个业务封装,如http的测试,是对远程方法的一个实现,对于页面测试,是对页面元素或操作的一个封装。
第三层测试用例逻辑层,该层主要是将服务层封装好的各个业务对象,组织成测试逻辑,进行校验。
第四层测试场景,将测试用例组织成测试场景,实现各种级别cases的管理、冒烟,回归等测试场景。 -
测试执行
随着测试用例的丰富和完善,测试时间会越来越长,我们的原则是尽量让所有的cases在15分钟内执行完,让整个发布流程能快速迭代起来,在实践中采用了两个方法:
测试调度:主要用于集成测试和UI测试,因为这两个是对一个系统整体进行测试,RDC采用微服务的架构,整个RDC应用被拆分成十几个微应用,如果一个应用发布后测试过程中,另一个关联应用也发布了,那么这次测试结果的意义就不大。
因此在执行中加入了调度机制,对关联应用进行调度,比如A、B是两个互相关联的应用,A应用发布后正在进行测试,B应用发布,这时我们的调度程序会将测试中断,然后B应用发布完成后,重新开始测试,最后将结果返回给A、B两个应用,同时完成卡点。
分布归并: 在测试集合中我们将用例进行组合,保证每个测试集合都在15分钟内,一旦发现时间增加则将测试集进一步拆分,然后将这些测试集并行执行,最后将整个结果归并,既能压缩时间,又能实现报告的统一。 所有用例在docker中执行,动态的分配测试资源。
4.3 单元测试与依赖隔离
关于单元测试的投入产出不同的公司团队有不同的认识,实践中认为单元测试的主要意义在于最低成本的发现问题,尤其对于代码的改动,相比集成测试阶段的问题更容易定位,并且良好的单元测试习惯能够使开发在编写代码时更全局的考虑问题。单测中主要需要解决的问题是外部依赖的隔离。
-
外部依赖的隔离:Mock和Stub
由于单测主要检查自身代码,因此需要处理对外部依赖的隔离,常用的外部隔离方式主要有Mock和stub两种方式,这两种方式的主要区别是Mock关注对代码逻辑的验证,Stub关注状态的验证。单元测试中采用Mock的方式,原因是Mock能快速的实现单测逻辑,执行速度快,而Stub一般需要搭建复杂的模拟环境,维护成本较高。 -
隔离工具:Powermock+Mock
通过这两个工具可以Mock出 1. 构造函数、2. 静态函数、3.枚举实现的单例、4.选择参数值作为函数的返回值、5.在调用的mock方法中,改变方法参数的值。
使用中Mockito可以满足大部分的需求,但是它的实现机制是使用cglib来动态创建接口的类的实例,这种实现方式不能用于构造函数和静态函数,因为这两个需要使用类的字节码(比如使用javassist),所以结合PowerMock一起。 -
单测用例
使用TestNG的注释方式组织用例,通过Assert来校验逻辑数据是否期望。 -
接入RDC和覆盖率收集
在RDC的【变更】中配置【持续集成】实验室,在实验室中配置单元测试实验室,RDC会监控代码提交事件,一旦提交则触发单测的执行。
关于测试评价,目前也没有特别好的评价体系,所以最常用的还是代码覆盖率,在工程的pom.xml中引入cobertura,排除不需要计入的第三方class,RDC单测实验室中的代码覆盖率工具会自动统计覆盖率。目前RDC的行覆盖率在50%,据了解行业内做的好的一般在68%左右。
4.4 大集成测试
集成测试是整个持续交付中最重要的阶段,稳定可靠的集成测试能够减轻测试工程师的压力,其次能促进开发和测试之间的相互信任,形成良好的互动。集成测试一般采用API的方式,在预发阶段进行,选择这一阶段的原因是环境较为稳定,测试发现的问题能真正反映系统的问题。
-
集成测试的方式
如上面分层测试的框架中,采用httpclient作为工具,将远程的http服务封装成本地服务,再进一步实现测试用例和测试场景。 -
集成测试规模
集成测试按照集成的规模,分为小集成测试和大集成测试,小集成测试一般是在某几个系统之间进行集成,大集成测试在整个系统中进行,这两个主要区别是测试覆盖和测试时间,我们使用最在靠近用户UI一层的API进行大集成测试,这一层更能体现真实的体验,能用最少的投入产生最大的价值。测试时间通过测试调度控制,每一个子系统发布时检测是否有测试进行,如果有则停止再重新触发。 -
接入RDC
在RDC应用中有【测试验证】,选择【添加验证点】,设置阶段为预发集成测试(也可选择其他阶段),同时在此处可以设置卡点的条件,如设置测试通过率99%,当你的测试通过率小于这个数字时在发布流程的预发测卡点中将会报失败,防止将测试验证不通过的代码发布到线上。
4.5 云浏览器上的UI自动化测试
UI测试是一个很繁杂的事情,投入产出比不高,UI测试最大难点在稳定性和维护性方面,因此我们在UI功能测试当中只覆盖主流程和最基本的页面检查,主要对主流程进行覆盖,如增、删、改、查等这些出错会造成系统不可用的功能进行覆盖。
-
UI测试的实现
UI测试采用selenium,通过selenium时将页面元素和功能进行抽象,称为PageObject,如页面的增加按钮,查询按钮的封装作为PageObject放在服务层,而测试逻辑层则用这些服务组装成需要的测试逻辑。值得分享的是使用selenium做UI测试时尽量采用css,css定位较为准确,相比xpath定位速度较快,对比的结果是采用css比xpath速度最快能到20倍,尤其在IE上。UI测试另一个比较麻烦的事情是兼容性,由于各类浏览器的快速迭代经常造成selenium连接浏览器、获取元素失败。因此我们对selenium-grid进行了二次开发,搭建浏览器云,根据指定的浏览器类型,版本、以及selenium的版本等在指定的浏览器上运行,满足兼容性测试的同时,保证UI测试的稳定运行。
-
接入RDC
跟集成测试一样,通过将UI测试配入发布流程进行卡点;实践中是通过RDC实验室的多阶段将集成测试和API测试同时配到了预发环境,完成测试卡点。
4.6 全站式UI页面校验
如上面提到的UI功能测试主要针对主流程功能性的逻辑测试,无法对页面元素和展现进行校验,因此我们对UI进行地毯式的校验,采用的方法是 页面元素+图像比对。
基本步骤:
第一步:使用selenium对RDC的各个页面进行爬虫,深度为主域名以下三级Link,并遍历出这些页面的元素,主要对文字、Button、Link、Form元素进行收集,保存到数据库中。
第二步根据前面录制的Link,分别检查页面能否正确打开,并校验这些页面是否包含存在的元素。
第三步爬虫脚本完善,由于个别页面的变化和对页面排版的要求,对导致测试失败的脚本进行完善,去掉动态元素,由于动态元素不是很多在整个测试过程中影响不大;另一个是对展示要求较高的页面加入图像比对,提前对屏幕进行截取(分块截取,动态内容不做截取,这部分在前面的功能测试中进行覆盖),在回放中会对相应页面进行截取,使用工具sikuli将两个截取的图片进行比对。
4.7 核心接口压测自动化
曾经有个故障,原因是开发的修改导致接口变慢。因此我们将压测加入到常规的测试卡点中,在每次系统发布前都会进行一次压测,将RT值进行对比。
-
(1).压测脚本采用jmeter,在jmeter中维护系统的关键接口,将jmeter的压测结果用csv保存,设置测试执行的时间和并发线程数,一般设置为10个并发,时间为8分钟。
-
(2).准备基准数据,基准数据为系统在稳定情况下,将上面的脚本执行10次,这10次执行的rt值为基准的数据,并将每个接口的基准数据保存在一个文件中。
-
(3).测试结果对比,如此每次jmeter的运行后会产生一个csv的测试结果,将这些结果与之前的基准数据进行对比,如果结果落在的基准期间或者未超过最大基准数据10%(根据不同接口的响应时间这个百分比不一样),则认为这次测试通过,并将这次通过的结果也插入到基准数据中,如此不断更新基准数据,最终保留30次的执行数据。数据处理采用pyton的pandas。 如果测试不通过则发送对比图片给相应的开发同学。 如下图反映了一个接口的异常响应时间。
-
接入RDC
通过在预发阶段新建【自定义实验室】 可以将jmeter的运行脚本,结果处理以命令行的形式配置到实验室完成测试卡点。
4.8 移动端真机测试
目前针对Android主要采用instrument的方式,对ios则采用自带的的sdk的方式,随之而来的问题是针对不同平台需要两套测试代码。开源的macaca和appium解决了这个问题,这里选用macaca来进行测试,相比较Appium优点是运行稳定,去除了appium老版本Android的支持,而且macaca有专门的团队进行支持。使用macaca能够像selenium一样对移动端的界面进行测试。
-
真机测试环境搭建
起初为了节省成本,采用了android和ios的模拟器,实践中模拟器的可用性太差,各种启动慢,异常退出,基本无法使用。最终采用了jenkins+服务器+真机的集群方式,服务器安装jekins的slave,ios和android环境,手机接入服务器,并在服务器上安装macaca服务。 -
测试执行
如上采用jekins中央集群的方式,通过jenkins来配置要进行测试的机型,jenkins根据机型查找手机所在的服务器和手机的uid,并将测试代码下载到该服务器,执行测试脚本,并返回结果。 -
接入RDC
目前RDC尚未提供真机环境,因此我们使用Jenkins管理真机环境,然后通过调用Jenkins的Rest接口,触发测试在真机环境上运行,执行完成后解析Jenkins的报告为RDC格式的结果,即可在RDC的实验室中展示测试结果,进而实现对发布流程的卡点。
4.9 AOP录制回放进行API测试
传统的API测试,一个接口一个接口的编写代码,准备测试数据,添加验证点,工作量大,覆盖不全,测试效果不明显。这里我们引流生产环境的流量来进行API测试,原理是AOP的方法拦截。主要有三个步骤,线上流量复制,回放,写操作的Mock。
-
录制回放
录制:使用ASM修改被测接口的字节码,侵入生产环境(生产环境提供一台机器),在每个被测方法执行前拦截得到需要的参数,方法结束时拦截得到返回的响应值。将录制的返回值和方法序列化后保存到数据库,需要注意的是对写操作和外部系统的调用可能会对生产数据造成破坏,所以对接口内部的方法依次进行跟踪,跟踪到写操作时,录制写操作返回的数据。 -
回放:同样采用ASM在测试环境(Beta环境)修改被测接口的字节码,触发接口的调用,在被测接口中使用上面录制的参数,接口执行完成后将录制的结果和这次执行的结果进行对比,结果一致则测试通过。
Mock:对于写操作,由于我们采用的Beta环境和生产环境采用同样的数据库和外部系统,因此到写操作不能真实的写入数据,需要在写操作的方法中直接返回上面录制的写操作数据。 -
接入RDC
在上面的录制环境和生产环境中,安装agent开启http服务,跟RDC实验室进行通信并控制测试的执行,最后的返回结果被RDC解析,从而实现发布卡点的功能。目前该系统正在改造,改造后将对云上用户开放。
4.10 测试报告
测试报告同样采用了分层的报告,分为:实时报告,单次执行报告,统计分析报告。
-
实时报告
主要针对线上用例的冒烟测试,原因是测试用例全部跑完需要一定的时间,而对于线上我们希望能尽快的将错误反馈给开发并解决,所以一旦有错误就会实时将错误信息和的现场通过钉钉发到开发群。 -
单次执行报告和后台日志
单次的执行主要是对这次运行的结果做展示,包括测试用例,测试步骤,截屏等。 在发送报告的同时我们会拉取测试时间段应用的后台日志,这些日志包括了错误所在的机器,出现的时间点,stack以及,各个应用接口间的数据传递,通过这些日志能方便开发同学快速定位问题。 -
统计分析报告
在测试过程中同样需要对测试代码的改进和测试稳定性的提高,每次测试完成后我们会将这次测试的结果解析发送到我们的一个测试报告系统中, 对通过率,失败次数等进行统计分析,通过一个时期的分析来找出值得改进的测试用例或系统功能,从而提高测试稳定性和系统稳定性。
下图是几种测试报告的一个图片合集:
5 线上质量监控
5.1机器和业务监控
对于机器监控有很多开源的方案,在此不再详诉,一般来说主要采集机器的CPU,内存,TPS,JVM等指标,一旦某个指标跨过红线,及时做出报警。
对于业务数据比较复杂,根据RDC的业务有几十种不同的监控指标,这里我们主要采用的是日志打点的方式,RDC的各个子系统的都对输出日志进行了改造,生产环境的每台机器会有一个专门解析日志的agent,这些agent增量的实时监控业务日志,并将日志解析得到跟业务相关的数据,发送给监控系统,从而监控系统能得到某个点或者某个时间段内的业务情况,根据划定的业务指标红线给出报警。 该系统目前正在进行改造,之后将提供给云上用户使用。
5.2线上日志聚合分析
由于现在系统都采用微服务的方式,系统拆分较细,每个微服务又都采用集群的方式运行,因此系统只要正常运行,后台发生了多少错误几乎没有人关心,而这些错误有可能是导致故障的潜在隐患。
我们采用了阿里云的sls日志收集功能,将RDC相关应用的日志接入到sls,每天晚上00:00按应用对RDC当天产生的日志进行聚合分析,目前通过一定的算法对错误信息进行聚合,发现问题和缺陷,自动提交bug,并发送当天的统计报告。 目前通过日志的聚合分析,RDC修复了大量稳定性方面的错误,错误数由之前每天的十万量级降低到现在的千级。
5.3 SLA服务数据驱动质量
将用户体验量化,如何为用户提供极致体验,一直是我们努力的目标,然后由于RDC上下游系统较多,如何评价现有系统,采用将服务体验数字化,通过将每一层的服务数字化来找到问题的瓶颈。
数据的收集采用了两种方式,一种是直接从数据库中提取数据,另一种是通过打点的数据提取业务数据。将这两种数据经过计算统计报表的形式展现,从而清晰的知道目前系统的情况。
如图是某一周的业务数据:
通过这些数据能够了解到整个系统的瓶颈,从全局了解系统和用户感知的差距,并指定目标和改进计划。 目前的问题是无法智能的拿到这些失败或错误的原因,比如出版本失败率0.14%,这0.14%是什么原因导致的需要花很大的精力排查,如何智能的找到问题,减少排查工作是下一步要做的工作。
5.4 面向业务的稳定性监控和故障演练平台
-
功能监控,将RDC相关系统的冒烟用例在线上7*24小时连续不断运行,来及时发现线上的问题,尽量在先于用户发现问题避免故障面的扩大。
-
页面监控,同样对RDC的页面7*24小时访问,看这些页面是否能够正常访问,链接是否被修改。
-
故障演练,根据RDC系统和业务逻辑制定专门的故障演练方案,将这些演练场景验证点用自动化实现并接入到故障演练平台。目的是能实现可靠地自动化故障演练,从而让演练按需随时进行,发现复杂场景下潜在的故障。
6 .研发质量规范和提升
研发质量的提升目前主要包括代码审核、代码质量检查好研发效能数据的回溯。
-
代码审核:在RDC上创建变更时会指定响应的代码审核人员,发布时指定的开发同学会对发布的代码进行审核,除了保障质量外,最重要的是通过审核提供开发人员的代码质量和代码规范,减少较低级的错误。
-
代码扫描:我们Sonar扫描+阿里巴巴集团规约扫描的方式,要求对Blocker和Critical的问题必须修复后才能上线,其它级别的缺陷不能超过20%。
-
研发效能回溯:RDC提供的度量功能是一个团队的研发情况看板,它能够反映研发团队在处理需求,缺陷、发布、回滚以及有效代码量等方面的情况,从多维度的统计一个团队的研发能力和研发效率。通过每周回溯这些数据,找出团队存在的问题,促进研发效能的提高。
7. 总结和展望
对于自动化测试,希望能用更有效的发现问题,尤其在UI测试上,考虑如何用最低的成本获得最大的回报,进一步完善爬虫式测试回放的工具,增加测试的覆盖率。
线上质量的监控,希望能够对故障进行预测,目前我们做到的还是对故障的处理,比如故障发生10s内发现,3分钟能响应处理完成,但是如果能够对故障进行预测将极大的降低系统风险,所以下一步工作是对海量错误日志,故障数据,用户反馈数据,监控数据进行数据挖掘分析,并结合实时监控建立自学习的预警系统。