欢迎关注我的新博客地址:http://cuipengfei.me/
我所在的项目的技术栈选用的是Play framework做后端API,前端用Angular JS。
由于用了Scala和Play。构建工具非常自然用的就是sbt。
而因为前端用了Angular,所以functional test就选用了和Angular结合较好的protractor。
这一切看起来似乎非常美好,一个无状态的后端,一个数据和UI双向绑定的前端。
What could possibly go wrong?
一開始也确实如此,没什么问题。我们为了让functional test在CI上跑起来。写了一个脚本来把play dist打出的包部署到CI所在机器上,然后执行protractor。
这个脚本执行还算ok。偶尔有点小问题,修一修也就好了。
只是。这也就是说有两个因素可能会使得我们的CI挂掉,一个是用sbt跑的specs2的測试,一个是protractor的測试。而protractor的測试是基于我们自己所写的脚本的,与sbt没啥关系。
麻烦来了
上周五的show case,我们一个小时后就要给客户演示现有产品的演示样例。可是CI挂掉了,新的代码没法走到QA和UAT的环境。bug fix也过不去。
最后我们不得不改动jenkins的配置,把sbt test和protractor的test都临时禁掉。才让最新的代码到了UAT上去。而这一切。是在show case之前一分钟才解决掉的。
事情总是这种,出一两次小问题,修改动改就好。我们不会注意到其危害,不会想到其风险。
直到琐碎的积累导致了严重的后果,我们才会正视问题的存在。而这个时候问题也许已经复杂和严重到不可修复或者是要花非常大成本修复的地步了。
保持一个健康的CI是如此。写代码的每个细节也是如此。
还好,非常幸运,我们的问题还没有那么严重,还来得及修。
在决定要修之后,先
定义一下问题是啥
往简单里说,就是CI不稳定。动不动就随便挂。
说的再细一些,就是我们手写脚本去做部署和測试这件事算是又一次发明了轮子。而这个轮子不如已有的经过打磨的轮子那么静止仔细,那么稳定好用。
以至于我们的CI偶尔就要出格一次。
Ok,问题定义清楚了,那么想想解决方式吧。
可是。在提出详细的方案之前,先想想,假设把这个现时还未存在的解决方式作用在现有问题之上,会收获一个什么样的结果呢?
验证标准
基于以上所述,我想解决问题的方案要满足下面3点:
- 能让CI重回稳定
- 一条命令行运行整个build
- 不要再自己造轮子了
第1,2点毋庸赘言,这就是我们问题的核心。关于第3点。是由于我们没有时间精力。也实在没有必要造这个轮子,假设能找到现有的轮子可以解决这个问题,并且还比我们自己的木头胶皮轮子好用。那岂不妙哉?
于是,我要開始寻找一个能让CI重归稳定的神圣轮子了!让探险的旅途就此展开吧!
開始寻找轮子
我最初的想法是用play的test framework,当中已经集成了selenium,用来做functional test非常是合适。
可是因为我们基于protractor的測试数量已经不少了,所有重写成本较高。所以这个轮子就放弃了。
残念,再看下一个轮子
再然后我想到的是自己定义一个sbt的task,这个task依赖于sbt已有的run。
这样就能在我的task启动之前把play跑起来。而task本身执行protractor的測试。再之后则杀掉正在执行的play app。
看起来不错,可是有问题:
第一。sbt run跑起来后是不会自己退出的,它会维持play一直在待命的状态,这也就是说我自己的task根本就没机会运行。
第二。即便能找到方法让我自定义的task和run同一时候跑起来,protractor执行完成后还要关掉run。免得占用port。
这又是一件麻烦事儿。
于是,这第二个轮子也被我自己给枪毙掉了。
再次残念,还有轮子吗?
会有的,总会有的,仅仅要肯去找。还是会有的。
这次我想到,写sbt的task不成。那就写代码。
我写个specs2測试。在case里用代码启动sbt run。然后再启动protractor,最后关闭sbt。总行了吧?
这样,确实是能够work的,而实际上我也把它做出来验证了能够work了。可是缺点非常大。
第一,因为我们的specs2測试都是用sbt跑的。而在当中再启动sbt,相当于要开两个jvm。消耗非常大。在我本地机器上能够压榨的仅仅剩两位数的内存。
第二,在sbt已经编译好了产品代码和測试代码測试之后。再開始跑还有一个sbt run,会导致sbt把代码又一次编译一遍。而Scala的代码编译是非常慢的。
我试了一下。这两次启动sbt。两次编译所消耗的时间是四分钟左右。时间成本太高。
CI的速度会被拉下来,受不了。
基于以上两点原因,我的第三个轮子也被我自己枪毙了。
命途多舛啊,三次尝试都失败,以你为我要放弃了吗?哼~~~
最后,我结合第一次的尝试和第三次的尝试找到了一个惬意的答案。
来看代码吧:
就仅仅有这么一点点代码。
running和TestServer都是play的test framework所提供的API。
顾名思义,其作用就是把play的app跑起来。
然后发一个get请求。assert它的response的status是200,以此来确保play真的是把server执行起来了的。
再然后执行shell脚本,把protractor跑起来。
这里Scala会做implicit conversion。把字符串转换成ProcessBuilder,从而能够调用其run方法。
最后assert。protractor的shell脚本是返回了0的。意味着functional test跑成功了。假设protractor測试挂掉,返回了1。那么specs2的这个測试也会挂掉,从而挂掉整个build。而这,正是我想要的。
这个解决方式合规吗?
检验一下吧。
因为server的启动和关闭都是有play的test framework的API负责的。比自己手写得脚本要稳定,所以符合了重归稳定性这一点。
因为用了specs2的測试,它能够跑在sbt里,所以符合一条命令跑build这一点。
整个解决方式仅仅用了specs2和play的test framework,没有又一次发明轮子,所以这一点也符合了。
除了符合最初定下的三条标准之外。还有额外的优点:functional test所跑到的代码会被纳入到測试覆盖率里面去。
由于和其它specs2的測试一样,protractor的測试也在sbt jacoco:cover的监视下跑的。所以自然就纳入了coverage的范围。
Takeaway
在解决问题之后,我想我会有三点takeaway:
-
多尝试几种方案,不要随便放弃。即便想,也不要。
-
不要屈就于working solution,要相信一定存在你如今还没想到的更好的方式。
-
反复发明轮子总是会显得非常诱人。由于它看起来能够非常直接并且准确的解决我们的问题。而实际上它经常是直接并且准确的解决我们的问题的现象。
假设能找到现象产生的原因。干死这个原因,问题的解决也许会更彻底。