如果说在工作里有个事情一直在颠覆我的认知,那就是 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):
单元测试方面 (scala)
接口测试方面:
UI 自动化方面:
说起 CI,那就离不开分支模型。一个好的分支模型是做 CI 的基础。我先来描述一下我见过的分支模型吧。
在软件开发中,持续集成能够解决的问题是尽早的集成和尽早的反馈。因此,尽管目前流行的所有版本控制工具都提供了分支/合并功能,但在团队和项目都比较小的时候,还是使用单分支开发策略是比较适合做 CI 的。这样会减少多分支在合并时的开销,也减少了 CI 消耗的资源。这时候 CI 的模式图大概是下面这个样子 (一个朋友的图,我自己懒得画了)
这个是比较理想的模式,RD 在 check out 代码后进行开发,开发完成后在本地自动化运行测试。证明新开发的功能在本地是没有问题的。但是在这之前其他的 RD 不停的往分支上 push 代码。所以这时候才需要从分支中在将最新的代码 merge 回本地再进行一次自动化测试以保证自己的代码不会跟最新的代码冲突。最后 push 到分支上,分支的代码配置了触发式的自动化测试。当监控到有新的代码更新,自动化部署和测试就会运行起来做最后一道检查。
我来解释一下上面的行为,可能现在有人会觉得困惑。尤其是在拉取最新代码到本地运行一次自动化测试后,到了主干上还要做一轮测试。主要有以下几个原因
这时候对于自动化最大的压力在于应变的速度,尤其是环境部署架构的应变速度,因为所有代码提交都发生在一个分支上,变化会比较快,相较下来没有缓冲时间,构建失败影响的几乎是所有人。而且选择单分支模型的项目很多都还在快速发展期,产品在业务上和架构上变化会比较多。所以这时候对自动化的应变能力有很大的要求。不仅仅是测试,还有编译和部署。我们需要在架构上把自动化设计的比较好,不能开发那边随便改了一个地方,你这边就要改一大堆。那样跟本改不过来。如果你没自信做到好的应变,就只自动化那些最稳定的模块。别把时间浪费在都修改脚本的事情上。
上面就是 CI 的一个简短流程。不论是我们现在的单分支模型还是以后多分支模式开发。针对某一个分支的流程差不多都是这样的。其实在外企中还经常用到一个概念叫令牌,不过我不详细描述了,因为我实在没怎么实践过这个东西,只是当年外派到外企的时候用过,不知道是否合适用到我们这的环境下。这种单分支模型是初创公司常用的模式。
每个公司的开发模式都不一样,我介绍一下我们公司玩的模式。下面是分支模型图。
这是我们当初的分支模型图,我们目前的行为跟图上略有区别,但是区别不大。其实挺好理解的,我们没搞那么复杂。把 develop 分支当成主要的开发分支。 在 develop 分支的基础上拉出多个 feature 分支,以 feature 为核心各自组成 feature team。每个 feature 分支其实就可以当做一个单分支开发模型了,feature 开发并测试完成后 merge 回 develop。但是在开发中我们要频繁的从 develop 上 merge 代码到 feature 分支上。以尽早发现 feature 分支跟 develop 上功能冲突的情况,也是为了尽早发现 bug。只有通过了 CI 的代码才准许合并回 develop。这么做我觉得主要有以下几个好处。
这种多分支开发模型会对 CI 带来比较大的负担,分析一下有以下几点。
恩,可以看出来 ,这时候测试的压力比较大,但没办法,这是 CI 的代价。都是为了尽早暴露问题,尽早反馈。 我们也可以根据情况只维持一个 develop 的 CI。这样做的代价很小。但是有些问题都会很晚才暴露出来,这对发布版本来说就不是什么好事了,大家根据自己项目的情况抉择吧。
这个我简单说一下吧,因为其实没啥本质的变化。就是产品可能按模块,甚至按产品线划分了团队,代码都是放在不同的库里。 这很常见的,针对不同的模块,产品线做 CI 就可以了。 只是记得需要把整个产品都部署起来做测试也加进 CI 里,以证明他们相互合作是没问题的。这个我就不多说了。
各家的分支模型都不一样,CI 的方式也不一样。也许有比我们更高效的方式,但我确实没实践过。所以我只能介绍下我们做 CI 的方式,权当抛砖引玉了。
一般都是 3 种方式配合着使用,这方面没什么太好说的。
我们说想做好 CI 是比较难的,因为它是一系列细节的集合。想做好它就要把很多很多细节做好。例如流程,沟通,代码。想做好 CI 就不是单独 QA 一个角色能做好的。很多时候 CI 推不起来都是因为团队的工程文化导致的。例如 RD 不想写单元测试,或者大家觉得 CI 没必要,或者压根就没什么好的分支模型,有些时候 QA 确实挺无奈的。上面说的分支模型和流程是我们团队历经 1 年多的演变而来,这里面大家的功劳很大,好多东西是 RD 和产品人员自发推进的,我很感谢他们给了我这么有利的环境,感谢第四范式的小伙伴们。
好了,书归正传,虽然我们在现实中会碰到很多困难,但是我们自己也是可以做一些事情的。我们说细节决定 CI 的成败,我来说说从 QA 的角度哪些细节需要注意的,介绍一些小技巧。
前面说应变和维稳很重要,我们不希望在 CI 中频繁的修改大量脚本,那样会让人崩溃的。很多地方的自动化到最后都处于崩溃状态就是因为实在维护不过来了。我见过几千条脚本每次 CI 失败个几十一百的情况。这时候 CI 已经是负担了,里面有脚本不稳定的因素,也有开发做个小改动,这边失败了几十个 case 需要一个个去改的情况。所以我来说说自动化测试中应变和维稳的小技巧吧。
我们最常见的可控制的变化因素有哪些呢?以 UI 自动化为例
首先,把什么 error code,status code,error message 的都封装成枚举,其实开发也是这么做的,没准哪天他们重构的时候这些就都变了。如下:
严格规定所有需要用到这些概念的时候都使用枚举,在设计方法的时候就把枚举当做参数传递,或者定义一个类的时候把属性规定为枚举类型。总之,强制使用枚举。这样在变化的时候你只改这个枚举就可以了。
咳咳,说的烂大街的事了,而且没什么难的。但我这里还是强调一下。首先 page object 大家肯定都做过这件事,把每个 page 的元素查询方式都顶一个专门的 page class 里面。但是这样还不够,我们要把通用的操作都放在一个方法里。如下:
我把页面操作按算子划分,所有想设置模型预测这个算子的操作必须调用这个方法。可以看到里面有很多判断,是否要自定义字段,是否全选等。就是这个算子的所有操作路径都在这个方法里。因为分成多个方法难免代码重复,代码重复难免增加维护成本。大家看到我用的参数是一个对象类型,而不是基本类型。也许很多人的习惯是用 String,int 等等这些基本数据类型搞一个方法。但这种设计方式在遇到页面删减元素的时候就悲剧了,假如新加一个必填的元素,你要在这个方法里也加一个参数来控制这个元素。但以前的 case 还是按老的方法签名设置的调用。所以所有调用这个方法的 case 都会改。 而如果你定义了一个对象当做参数,这时候只需要在对象中增加一个属性,然后给个默认值就行了。这样很多 case 其实不用改。看一下这个对象的定义:
这个对象中还使用了枚举,我们看一下枚举的定义:
因为控件是按文案搜的,也就是 text(),所以把文案也封装成枚举,以防设计人员哪天看这个文案不满意要换一个。这就是我之前说的封装一切可控的变化。只要这个东西是多次使用的,就封装起来。当然这里其实也可以不用枚举,只是我习惯了。
如果某些功能,例如一些悬浮层是对所有页面生效的,或者一些操作是对所有页面生效的。可以把它放在 base page 里面,其他的 page 继承这个 page。如下:
这是我跟老外学的,老外管这种方式叫 stager,我理解就是后门。意思是使用一些其他的手段帮助测试把前置条件准备好。 还是以 UI 自动化为例,假如我要测试批注合同的功能,我需要先创建合同吧,不仅是评论,查询,删除,修改,状态转变等等都需要一个合同作为前置条件。如果都在页面上操作这个重复就太多了,创建一个合同可能要跳转 N 个页面,这不仅耗时,而且页面操作是有不稳定因素的,容易失败。如果前端页面出现 bug 也同样会导致一大批的 case 失败。 所以老外搞出了 stager 这个方式。跳过页面,调用产品代码或者直接访问数据库等方式制作一些通用的服务。帮助脚本创建这些前置条件。 他们的理念是我们专们有 case 校验这个页面,没必要让所有脚本都走这个页面,那太耗时了。所以那时候他们的自动化有两种,一种叫 BQ,意思是短小的 case 队列,不使用 stager,专门校验页面,逻辑简单。另一种叫 LQ,意思是长的队列,里面都是复杂的 case,使用 stager 创建前置条件。
再举一个轮询 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 的实践的讨论。