您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
持续集成 测试开发之路--持续集成
 
来源:TesterHome 发布于 2017-3-30
  1773  次浏览      14
 

前言

如果说在工作里有个事情一直在颠覆我的认知,那就是CI了。每个阶段我对它的理解都是不同的,每个阶段我都会这么想:恩,CI也就是这样了。但是项目发展一段时间以后我就又会想:哦,原来CI比我以前想的难的多。我相信在项目中实践完美的CI方案是每个QA的理想,可惜它就好似别人家的孩子一样,是我们心中的痛。所以今天我们调侃一下CI吧。本篇文章假设读者已经清楚的知道CI的概念和优点。所以就不在过多的介绍概念性的东西了。

工程文化

CI需要工程文化的支撑,它不是某个人某个职位的事情,它需要整个技术团队所有人的努力,是一种所有人都为产品质量努力的工程文化。对于QA来说,在推动CI的最初,也就是说服所有人CI的重要性以及澄清CI的形式是比较重要也比较难的。因为很多人的传统思想都是说测试是QA的事情,或者说大家都忙于加班,哪有时间写UT,又或者说本身就对CI这个事情有所质疑,甚至不知道CI是什么也时有发生,所这个时候澄清CI这件事本身就是比较重要的。需要让团队中的所有人慢慢转变他们的思想和工作方式。可能首先是先劝服项目经理和开发的老大,然后慢慢的去推动这件事。如果你已经处于一个良好的工程文化下,那么恭喜你,你已经走过了最艰苦的第一段路。如果你说服不了他们,或者成果比较小,过程也比较慢。那其实我们自己也是有一些事情可以做的。先从QA这里把CI的架子搭起来,营造一个良好的质量的生态环境后。 别人看到了CI的样子,是有助于他们理解你工作的重要性的。所以我们今天大部分介绍的都是QA做的事情。

高度自动化

想要做好CI就得高度自动化,这个大家都没意见。我们需要自动化编译,部署,运行单元测试,集成测试,功能测试。 只可惜单元测试这个最重要的组成部分在我们国内是很难推的。我相信大家都熟知下面这个金字塔形的自动化体系。

可惜理想是丰满的,现实是骨感的。单测在我们国内的工程文化中并不吃得开。所以往往我们做的都是一个倒过来的金字塔。这就在运行时间,稳定性和覆盖率方面有了极大的挑战。所以各位小伙伴,碰到肯在UT上下功夫的团队就要珍惜啊~(感谢第四范式的RD小伙伴,UT真的挺上心的)。虽然大部分的团队情况并不理想,但是该做的还是得做,我们还是可以在UI自动化和接口自动化上动手脚。

技术选型

这个可说的不多,无非就是市面上一些典型的工具和框架。我介绍下我们公司用的。

环境搭建方面:docker驱动,可以看一下我之前发的环境管理相关的文章。

单元测试方面(Java):

Junit:本来想用我熟悉的testng,但是开发的同学说测试springmvc只能用Junit。所以只能这样了

mockito:大名鼎鼎的java mock框架。解耦,提高覆盖率,行为测试的神器。

mockmvc:想测试springMVC的controller的话,只能用这玩意了

hsqldb:java的memoryDB,能够模拟真实的数据库,但是运行在内存中。单元测试的不二神器,提高运行速度,跟真实环境解耦。

jacoco:java的代码覆盖率神器。

单元测试方面(scala)

scala test: 相当于java的 junit,但同时把断言,mockito等等功能都集成进来了,是一个大的测试包

spark test:因为我们只用scala处理spark,所以不涉及到数据库,但是涉及到了spark的测试。这是个开源的spark测试项目,里面可以帮助你启动local mode的spark,并提供了一些RDD,DF等等的断言工具。缺点是运行的速度仍然不够快。

接口测试方面:

rest-assrued:代替http,它的api和独创的断言机制很赞

assertJ:java 断言神器,db的断言基本全靠它了。同时自动化测试中的数据恢复机制也是基于它实现的

testng:这个不用说了

allure report:高大上的report框架

还有一些小的,我自己封装的东西就不说了。

UI自动化方面:

selenide:基于webdriver的测试框架

其他的跟接口测试差不多。

分支策略

说起CI,那就离不开分支模型。一个好的分支模型是做CI的基础。我先来描述一下我见过的分支模型吧。

单分支模型

在软件开发中,持续集成能够解决的问题是尽早的集成和尽早的反馈。因此,尽管目前流行的所有版本控制工具都提供了分支/合并功能,但在团队和项目都比较小的时候,还是使用单分支开发策略是比较适合做CI的。这样会减少多分支在合并时的开销,也减少了CI消耗的资源。这时候CI的模式图大概是下面这个样子(一个朋友的图,我自己懒得画了)

这个是比较理想的模式,RD在check out代码后进行开发,开发完成后在本地自动化运行测试。证明新开发的功能在本地是没有问题的。但是在这之前其他的RD不停的往分支上push代码。所以这时候才需要从分支中在将最新的代码merge回本地再进行一次自动化测试以保证自己的代码不会跟最新的代码冲突。最后push到分支上,分支的代码配置了触发式的自动化测试。当监控到有新的代码更新,自动化部署和测试就会运行起来做最后一道检查。

我来解释一下上面的行为,可能现在有人会觉得困惑。尤其是在拉取最新代码到本地运行一次自动化测试后,到了主干上还要做一轮测试。主要有以下几个原因

本地环境和测试环境不一致,导致有些case在本地没问题,但放到server上就会出问题了。

运行测试后RD有时会忘记push某个文件的情况,需要在分支上做最后一道保险

现实的情况是依靠人为的自觉很难保证每一次都按约定运行测试,所以要求程序做最后一道验证。

分析一下

这时候对于自动化最大的压力在于应变的速度,尤其是环境部署架构的应变速度,因为所有代码提交都发生在一个分支上,变化会比较快,相较下来没有缓冲时间,构建失败影响的几乎是所有人。而且选择单分支模型的项目很多都还在快速发展期,产品在业务上和架构上变化会比较多。所以这时候对自动化的应变能力有很大的要求。不仅仅是测试,还有编译和部署。我们需要在架构上把自动化设计的比较好,不能开发那边随便改了一个地方,你这边就要改一大堆。那样跟本改不过来。如果你没自信做到好的应变,就只自动化那些最稳定的模块。别把时间浪费在都修改脚本的事情上。

上面就是CI的一个简短流程。不论是我们现在的单分支模型还是以后多分支模式开发。针对某一个分支的流程差不多都是这样的。其实在外企中还经常用到一个概念叫令牌,不过我不详细描述了,因为我实在没怎么实践过这个东西,只是当年外派到外企的时候用过,不知道是否合适用到我们这的环境下。这种单分支模型是初创公司常用的模式。

多分支多版本模式

每个公司的开发模式都不一样,我介绍一下我们公司玩的模式。下面是分支模型图。

这是我们当初的分支模型图,我们目前的行为跟图上略有区别,但是区别不大。其实挺好理解的,我们没搞那么复杂。把develop分支当成主要的开发分支。 在develop分支的基础上拉出多个feature分支,以feature为核心各自组成feature team。每个feature 分支其实就可以当做一个单分支开发模型了,feature开发并测试完成后merge回develop。但是在开发中我们要频繁的从develop上merge代码到feature分支上。以尽早发现feature分支跟develop上功能冲突的情况,也是为了尽早发现bug。只有通过了CI的代码才准许合并回develop。这么做我觉得主要有以下几个好处。

最大程度的保证了develop分支的代码质量。代码都是经过测试才合并到develop分支的。

把各feature team之间的互相影响降到了最低。每个feature都在自己的分支上工作。

版本发布与feature开发解耦,现在他们是两条线。单分支开发模型中feature开发和版本发布是绑定在一起的。某些重构或者周期较长或者暂时不打算发布的feature可以在独立的分支上开发。

分析一下

这种多分支开发模型会对CI带来比较大的负担,分析一下有以下几点。

测试代码跟随开发走多分支模型:大多数公司的测试代码都是单分支模型,但如果想做CI就必须跟随开发的分支策略。因为feature分支和develop分支的行为是不一样的,同一套代码不能运行在这两个分支里。需要跟开发一样单独开辟一个feature分支并在这个分支上写测试代码,结束后合并回develop。总归一句话,一切跟着开发走。

部署同样多分支模型:原因跟上面的一样,不过发生的频率很低,毕竟这个阶段部署方式不会频繁变化。

资源消耗:多分支模型,在每个分支上都要做CI。这对测试资源是有一定要求的。

测试代码的质量要求:单分支模型的时候要求测试代码应变要快,多分支模型的时候不仅要快,更要稳。 也就是说测试代码是稳定的,不能总随机性的失败。例如我们知道UI自动化是以不稳定闻名的,这时候你要用各种手段把他们稳定下来。不稳定的不要加到CI里。因为这时候同时存在很多分支,每天都运行很多次CI。出现错误你要一个一个去排查,错误多了会让人发疯的。以前单分支模型的时候跑的次数少,可能一天就一次,排查的压力小。现在这个问题被扩大了数倍。所以一定要稳。

对QA能力的要求:除了写代码以外,合并分支,解决冲突,code review,该走的正规流程可能都要走。多分支模型会把QA们打散到各个feature team里,也可能一个QA对着几个feature team。而且要求所有CI问题都在feature分支上就解决掉,所以这时候对QA们的平均水平有所要求,自己水平不行指望别人帮你的情况,最好还是别发生。

分支爆炸:像我们公司这种to b的业务。会有很多个release分支,这些release分支跟那些feature 分支不一样,他们很可能一直都不会被销毁。因为一个分支对应一个发布版本,对应着一批客户。不是每个客户都愿意升级版本的。所以如果出了问题可能要在release分支下追加fix来为客户解决问题。所以测试代码你也要保留这个release分支以防万一。so,分支爆炸的情况也挺烦的

恩,可以看出来 ,这时候测试的压力比较大,但没办法,这是CI的代价。都是为了尽早暴露问题,尽早反馈。 我们也可以根据情况只维持一个develop的CI。这样做的代价很小。但是有些问题都会很晚才暴露出来,这对发布版本来说就不是什么好事了,大家根据自己项目的情况抉择吧。

多模块多产品线模式

这个我简单说一下吧,因为其实没啥本质的变化。就是产品可能按模块,甚至按产品线划分了团队,代码都是放在不同的库里。 这很常见的,针对不同的模块,产品线做CI就可以了。 只是记得需要把整个产品都部署起来做测试也加进CI里,以证明他们相互合作是没问题的。这个我就不多说了。

分支模型总结

各家的分支模型都不一样,CI的方式也不一样。也许有比我们更高效的方式,但我确实没实践过。所以我只能介绍下我们做CI的方式,权当抛砖引玉了。

CI的执行机制

监控触发:监控到代码提交就触发一次构建,单元测试都用这种方式,因为单元测试运行快。

定时触发:每隔一段时间触发一次,对于运行比较慢的UI和接口测试,一般使用这种方式。

手动触发:在Jenkins或类似的平台上配置好任务,可能准许认为的手动触发一次。 例如开发想push代码前在自己的环境上跑跑冒烟测试。

一般都是3种方式配合着使用,这方面没什么太好说的。

细节决定成败

我们说想做好CI是比较难的,因为它是一系列细节的集合。想做好它就要把很多很多细节做好。例如流程,沟通,代码。想做好CI就不是单独QA一个角色能做好的。很多时候CI推不起来都是因为团队的工程文化导致的。例如RD不想写单元测试,或者大家觉得CI没必要,或者压根就没什么好的分支模型,有些时候QA确实挺无奈的。上面说的分支模型和流程是我们团队历经1年多的演变而来,这里面大家的功劳很大,好多东西是RD和产品人员自发推进的,我很感谢他们给了我这么有利的环境,感谢第四范式的小伙伴们。

好了,书归正传,虽然我们在现实中会碰到很多困难,但是我们自己也是可以做一些事情的。我们说细节决定CI的成败,我来说说从QA的角度哪些细节需要注意的,介绍一些小技巧。

自动化测试中的应变和维稳

前面说应变和维稳很重要,我们不希望在CI中频繁的修改大量脚本,那样会让人崩溃的。很多地方的自动化到最后都处于崩溃状态就是因为实在维护不过来了。我见过几千条脚本每次CI失败个几十一百的情况。这时候CI已经是负担了,里面有脚本不稳定的因素,也有开发做个小改动,这边失败了几十个case需要一个个去改的情况。所以我来说说自动化测试中应变和维稳的小技巧吧。

封装一切可能的可控的变化因素

我们最常见的可控制的变化因素有哪些呢?以UI自动化为例

参数个数的变化,数据库中和页面上各种error code的变化,error message的文案变化,各种task,订单等等的status的定义的变化等等。

页面元素结构的变化,属性的变化,新增减元素的变化,各类元素的文案变化等等。

利用好枚举

首先,把什么error code,status code,error message 的都封装成枚举,其实开发也是这么做的,没准哪天他们重构的时候这些就都变了。如下:

严格规定所有需要用到这些概念的时候都使用枚举,在设计方法的时候就把枚举当做参数传递,或者定义一个类的时候把属性规定为枚举类型。总之,强制使用枚举。这样在变化的时候你只改这个枚举就可以了。

page object 老生重谈

咳咳,说的烂大街的事了,而且没什么难的。但我这里还是强调一下。首先page object大家肯定都做过这件事,把每个page的元素查询方式都顶一个专门的page class里面。但是这样还不够,我们要把通用的操作都放在一个方法里。如下:

我把页面操作按算子划分,所有想设置模型预测这个算 子的操作必须调用这个方法。可以看到里面有很多判断,是否要自定义字段,是否全选等。就是这个算子的所有操作路径都在这个方法里。因为分成多个方法难免代码重复,代码重复难免增加维护成本。大家看到我用的参数是一个对象类型,而不是基本类型。也许很多人的习惯是用String,int等等这些基本数据类型搞一个方法。但这种设计方式在遇到页面删减元素的时候就悲剧了,假如新加一个必填的元素,你要在这个方法里也加一个参数来控制这个元素。但以前的case还是按老的方法签名设置的调用。所以所有调用这个方法的case都会改。 而如果你定义了一个对象当做参数,这时候只需要在对象中增加一个属性,然后给个默认值就行了。这样很多case其实不用改。看一下这个对象的定义:

这个对象中还使用了枚举,我们看一下枚举的定义:

因为控件是按文案搜的,也就是text(),所以把文案也封装成枚举,以防设计人员哪天看这个文案不满意要换一个。这就是我之前说的封装一切可控的变化。只要这个东西是多次使用的,就封装起来。当然这里其实也可以不用枚举,只是我习惯了。

如果某些功能,例如一些悬浮层是对所有页面生效的,或者一些操作是对所有页面生效的。可以把它放在base page里面,其他的page继承这个page。如下:

为了稳定使尽一切手段

stager

这是我跟老外学的,老外管这种方式叫stager,我理解就是后门。意思是使用一些其他的手段帮助测试把前置条件准备好。 还是以UI自动化为例,假如我要测试批注合同的功能,我需要先创建合同吧,不仅是评论,查询,删除,修改,状态转变等等都需要一个合同作为前置条件。如果都在页面上操作这个重复就太多了,创建一个合同可能要跳转N个页面,这不仅耗时,而且页面操作是有不稳定因素的,容易失败。如果前端页面出现bug也同样会导致一大批的case失败。 所以老外搞出了stager这个方式。跳过页面,调用产品代码或者直接访问数据库等方式制作一些通用的服务。帮助脚本创建这些前置条件。 他们的理念是我们专们有case校验这个页面,没必要让所有脚本都走这个页面,那太耗时了。所以那时候他们的自动化有两种,一种叫BQ,意思是短小的case队列,不使用stager,专门校验页面,逻辑简单。另一种叫LQ,意思是长的队列,里面都是复杂的case,使用stager创建前置条件。

数据库操作和文件系统的操作做辅助

1.不管什么类型的测试,有些时候光检验接口返回值,或者页面返回信息是不靠谱的,验证不完全的。如果是UI测试你想要验证结果可能做不到或者要跳转很多页面。这时候也许直接验证数据库和文件系统反而比较靠谱。

2.有些接口是异步的,你需要等待它执行完毕后去验证结果。可你从接口返回值那里得不到执行信息,因为它是异步的。同样页面上你也得不到信息,或者想得到你就得频繁的刷新,或者又是跳转好几个页面。这时候还是直接写一段逻辑轮循数据库的状态比较靠谱。例如下面的样子:

再举一个轮询hadoop 集群任务的例子

页面的稳定

UI自动化中,再怎么用stager你也避免不了太多的页面操作。所以有些时候网卡一下,系统处理速度太慢或太快都有可能导致UI自动化的失败。所以在写page object的时候要慎重处理页面的操作。尽量避免硬等待,使用框架的轮询方法,如果出现错误话,重试一次等等。举个例子如下:

数据的稳定

我们在自动化中难免碰到case之间会有数据的冲突。例如某条case修改了产品的全局设置,某条case在会清理掉所有的数据,某条case在运行前必须要求数据是空的等等。一般我们都会在测试的before或者after方法里小心翼翼的做数据的清理。但仍然免不了想清理的数据清理不了。那么怎么办呢,如果我们有一种方式能自动的清理数据,而且可以让编写脚本的人选择清理什么数据,什么时候清理数据。那就省事很多了。如下:

这里我定义了一个注解,注解的数据恢复策略选择的是method,意思是每运行一个case后,都把数据恢复成case执行之前的状态。还有一个级别是class,意思是等当前class中所有的case都运行结束后,把数据恢复到这些case执行前的状态。这样可以保证不会因为当前case的执行会产生或改变什么数据会影响到之后的case。当然也可以事先创造一批测试数据,例如订单,之后针对这个订单的任何操作都加上这个注解。这样所有case都可以使用这个订单测试而不担心会被其他case影响。具体的实现方式大家翻一下我之前写的一篇关于assertJ的用法监控式数据管理

代码复用的细节

上面介绍了应变和维稳的细节,现在我们谈谈代码复用。代码复用的越好,我们的维护成本越少。我介绍两个小技巧。

数据驱动

测试用例要做成数据驱动这个大家都知道,这样可以减少代码重复。 所以大家一般都会把测试数据都放在一个文件里。 这里需要注意一点的就是我们刚才讲的我们为了应变封装了很多枚举和对象。但从文件中读取出来的东西都是基本数据类型。每次读取出来再封装成对象这个成本太高了。 所以我是这么做的

我从文件中读取出来的时候就转换成对象类型了。文件中定义了很多组参数。这样很多的case我就用一条脚本执行了。并且我不需要做类型转换。 具体的实现细节请大家翻翻我以前的一篇关于数据驱动极其变种的文章吧。

断言定制

在测试中还有一个有很大代码量的地方就是断言,在接口测试中,尤其是RPC接口测试中,我们的断言会比较复杂。因为返回值复杂,以前测试dubbo接口的时候,发回的java对象特别复杂,一个元素一个元素取出来做断言那太痛苦了。如果你转成json做对比,又处理不了很多情况。假如某些字段是随机生成的,每次都不一样。或者说json太长做字符串对比,报错信息很难让你分清是哪个字段错误的。 所以我觉得做成下面这个样子比较好。

我以前写的代码了,截个图,上面就是比较两个task对象是否相等,这是一个copy task的接口,我们要比较两个从数据库中查询出来的task 对象是否相等,因为是用mybatis查询出来的,所以都封装成了对象,第一行代码是创建一个责任链对象,第二行代码规定了什么东西不需要验证,因为task的ID是随机生成的不可能相等。最后我们把两个对象仍进去就行了。你不用管它怎么验证的,责任链在运行到javaBean类型的时候,就会用java反射解析两个对象的每一个属性并调用链表中其他的节点做相应的断言。不仅仅是javaBean类型,JSON,数组,List,Map,File你全都能不管三七二十一的仍进责任链里。以前我们最怕的就是一个ORM映射出来的字段百八十个的,光是写断言就写到手软。现在完全木有这个问题了。如果有新的验证类型出现,你只需要在责任链表里增加一个节点对应这个类型作验证就好了。我在测试开发之路----可读性,可维护性,可扩展性中写过实现方式。大家可以看一下。

#总结

CI中我们说工程文化是灵魂,流程是核心之一,分支模型是核心之二,自动化是核心之三。我分享了一些我做CI中的一些经验和小技巧。其实CI还有很多方面,例如CI工具的使用配置(如Jenkins),一些辅助工具的使用(如代码覆盖率),CI 就是个一堆细节堆砌出来的东西,做好所有细节就才能好CI。 我们做的还有很多不足,希望以后能慢慢做的更好吧。也希望我这篇文章能引出更多的对CI的实践的讨论。

 

   
1773 次浏览       14
相关文章

为什么要做持续部署?
剖析“持续交付”:五个核心实践
集成与构建指南
持续集成工具的选择-装载
相关文档

持续集成介绍
使用Hudson持续集成
持续集成之-依赖管理
IPD集成产品开发管理
相关课程

配置管理、日构建与持续集成
软件架构设计方法、案例与实践
单元测试、重构及持续集成
基于Android的单元、性能测试
最新课程计划
信息架构建模(基于UML+EA)3-21[北京]
软件架构设计师 3-21[北京]
图数据库与知识图谱 3-25[北京]
业务架构设计 4-11[北京]
SysML和EA系统设计与建模 4-22[北京]
DoDAF规范、模型与实例 5-23[北京]

集成与构建指南
项目管理:Maven让事情变得简单
持续集成工具hudson
持续集成
Maven权威指南
程序集(UML中的包)之间循环
更多...   


产品发布管理
配置管理方法、实践、工具
多层次集成配置管理
使用CC与CQ进行项目实践
CVS与配置管理
Subversion管理员


海航股份 重构及持续集成
电研华源 设计原理、建模与重构
软件配置管理日构建及持续集成
单元测试、重构及持续集成
中国软件研发中心 单元测试与重构
单元测试、重构和持续集成实践
罗克韦尔 C++单元测试+重构+Gtest
更多...