|
|
|
|
# 22|设计实战(二): 一个全周期自动化测试Pipeline的设计
|
|
|
|
|
|
|
|
|
|
你好,我是柳胜。
|
|
|
|
|
|
|
|
|
|
说起交付流水线,你可能立马想到的是Jenkins或CloudBees这些工具,它们实现了从Code Build到最终部署到Production环境的全过程。
|
|
|
|
|
|
|
|
|
|
但Jenkins只是工具,一个Pipeline到底需要多少个Job,每个Job都是什么样的,这些问题Jenkins是回答不了的,需要使用工具的工程师去思考去设计。在实践中,通常是DevOps工程师来做这个设计。
|
|
|
|
|
|
|
|
|
|
既然我们学习了微测试Job模型,也知道了它能帮助我们去做自动化测试设计,那用这个模型,能不能帮我们做Pipeline设计呢?**其实,Pipeline本质上也是一个自动化测试方案,只不过它解决的场景是把软件从代码端到生产端的自动化。**
|
|
|
|
|
|
|
|
|
|
掌握设计思维是测试工程师向测试架构师的必由之路,假设今天你需要设计一个Pipeline,把一个Example Service的代码,最终部署成为生产环境的一个服务进程。完成这个工作,你不仅能弄明白CICD的原理和实现,而且对自动化测试Job怎么集成到CICD,也将了如指掌。
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/fc/bd/fc734fc77fcac6773de057d5d7437dbd.jpg?wh=1920x791)
|
|
|
|
|
|
|
|
|
|
## Example Service的Pipeline的目标
|
|
|
|
|
|
|
|
|
|
还是按照Job模型的设计原则,先理出Pipleline的第一个根Job。
|
|
|
|
|
|
|
|
|
|
Pipeline的起点在哪里呢?很显然,它从一个GitLab的repo开始,准确来说,要能访问Example Service的代码。怎么能访问代码呢?我们需要知道两个信息,代码仓库的URL地址和访问的token。
|
|
|
|
|
|
|
|
|
|
1.GitLab的代码仓库地址:[http://gitlab.example.com/sheng/exampleservice.git](http://81.70.254.64/yuzi/dockerbase.git)
|
|
|
|
|
|
|
|
|
|
2.访问token:token-123456
|
|
|
|
|
|
|
|
|
|
Pipeline的终点到哪里呢?很简单,最后Example Service能够在生产环境运行起来,运行起来的标志,就是Example Service的工作URL:[http://prod.example.com](http://prod.example.com)
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/c0/09/c05f9271d3a8dc72d8e16628d5690f09.jpg?wh=1920x791)
|
|
|
|
|
|
|
|
|
|
## 划分Pipeline的阶段
|
|
|
|
|
|
|
|
|
|
这个从代码转成生产环境服务的过程,至少要完成两次转换。
|
|
|
|
|
|
|
|
|
|
第一次转换是**把代码转成可部署包**,第二次是**把可部署包转换成运行态服务**。为了把好验证关的大门,我们还要在这两次转换中间再加入一个测试任务,那么Pipeline就可以分解成3个子Job,分别是开发Job、测试Job、部署Job,它们依次完成,全部运行通过,最后才能部署到生产环境。如下图:
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/a6/bb/a698bd4c9cbe0912fe9f970c4de5fabb.jpg?wh=1920x791)
|
|
|
|
|
|
|
|
|
|
这三个Job,我们依次来看一下它们都需要干什么活。
|
|
|
|
|
|
|
|
|
|
首先,我们来看看DevJob。开发阶段测试Job,作为整个Pipleline的初始Job,Pipeline的Input就是DevJob的Input。
|
|
|
|
|
|
|
|
|
|
1.GitLab的代码仓库地址:[http://gitlab.example.com/sheng/exampleservice.git](http://81.70.254.64/yuzi/dockerbase.git)
|
|
|
|
|
2.访问token: token-123456
|
|
|
|
|
|
|
|
|
|
DevJob要做什么呢?它前面连接了代码库,后面要对接集成测试,它的主要职责就是把代码变成可部署的Package。
|
|
|
|
|
|
|
|
|
|
Input有了,DevJob要输出什么呢?我们要知道,DevJob的输出会作为下一阶段Job的输入,设计DevJob的输出就跟接口设计一样,不同的接口都能让系统工作,但我们要选择最合适那一种。
|
|
|
|
|
|
|
|
|
|
比如,我们可以让DevJob输出一个Example Package的仓库地址,代表Package已经打好了,并上传到了仓库,或者我们输出单元测试报告;甚至,DevJob也可以什么都不输出,让下一个TestJob自己去按照双方约定好的Ftp服务器上去取包。
|
|
|
|
|
|
|
|
|
|
哪种方案更合适呢?
|
|
|
|
|
|
|
|
|
|
这里你要考虑两点:第一,我的输出别人是否用得到?像单元测试报告这种信息,是我DevJob内部的工作产物,别人不会感兴趣。按照面向对象设计原则,这些细节应该隐藏在DevJob内,不向外公布。
|
|
|
|
|
|
|
|
|
|
第二,我的输出是不是信息显式而充分的?像多个Job都遵循一个约定,去指定的FTP服务器上去取放Package,这个信息交互就是隐式的,没有在接口上体现出来。这样是有隐患的,如果之后各个Job变更重构的时候,就会出问题。
|
|
|
|
|
|
|
|
|
|
根据这个原则,我们让DevJob输出一个Example Package的仓库地址
|
|
|
|
|
|
|
|
|
|
DevPackageURL: [http://nexus.exmaple.com/content/repositories/exampleservice\_1.0.1.jar](http://nexus.exmaple.com/content/repositories/exampleservice.jar)
|
|
|
|
|
|
|
|
|
|
如果DevJob输出了这个URL,就代表DevJob已经运行通过,把代码变成了Package,而且把Package放到了组件管理平台Nexus上去,别人就可以用了。
|
|
|
|
|
|
|
|
|
|
你可能会问,拿到了Package URL还需要Nexus的用户名密码才能访问。那下一个Job从哪里获知用户名和密码呢?这时,就可以用到我们Job测试模型的TestConfig属性了。直接把用户名、密码放在TestConfig就可以了。
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/d1/82/d170896468d28932a5eb07745fe79482.jpg?wh=1920x755)
|
|
|
|
|
|
|
|
|
|
同样的思路,我们也能整理出TestJob的Input是DevPackageUrl,Output是ReleasePackageUrl:
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/22/b4/22ca2574e08a1622293e08bcfdc250b4.jpg?wh=1920x755)
|
|
|
|
|
|
|
|
|
|
DeployJob的Input是ReleasePackageUrl,作为Pipeline最末一个Job,Pipeline的输出就是DeployJob的输出:
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/85/b2/850eac96f93e65yy59d0124cac813db2.jpg?wh=1920x755)
|
|
|
|
|
|
|
|
|
|
推演到现在,整个Job树就成了后面这样:
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/d0/35/d08104f939e9ff47096974d761395935.jpg?wh=1920x713)
|
|
|
|
|
|
|
|
|
|
我们把Pipeline拆分成Dev、Test和Deploy三个Job后,就可以把它们分别指派给开发人员、测试人员、运维人员,由他们来推进下一阶段的设计了。至此,概要设计告一段落。
|
|
|
|
|
|
|
|
|
|
## 各阶段Job的详细设计
|
|
|
|
|
|
|
|
|
|
我们在上面的设计中规划了3个阶段,明确了作为Pipeline job的第一级子Job:DevJob、TestJob、DeployJob。但每个阶段要做什么具体任务,还需要进一步详细设计。
|
|
|
|
|
|
|
|
|
|
### DevJob
|
|
|
|
|
|
|
|
|
|
我们先看DevJob,它的Output是一个可执行包。
|
|
|
|
|
|
|
|
|
|
怎么完成从代码到可执行包的转换呢?当然要进行构建,也就是Build。
|
|
|
|
|
|
|
|
|
|
BuildJob的模型很容易梳理,它的Input是codeRepoUrl,而Output是DevPackageUrl。BuildJob从代码库里抓取代码,构建,然后打包生成Package,上传到对象仓库,就完成了。
|
|
|
|
|
|
|
|
|
|
但build通过了后,是不是可以直接产生DevPackage呢?这里我分享一个“保险”措施,为了保证质量,我们可以在build之后,加上一个测试任务,也就是单元测试UnitTestJob。单元测试通过之后,再输出DevPackage。
|
|
|
|
|
|
|
|
|
|
UnitTestJob就是运行单元测试,单元测试成功还是失败,就是UnitTestJob的结果。所以,UnitTestJob的Job模型里,只设置一个Dependency就好了,指向BuildJob。BuildJob成功之后,才能运行UnitTestJob。这里的UnitTestJob,我们选用Junit开发框架就能实现。
|
|
|
|
|
|
|
|
|
|
现在DevJob被细分成了两个子Job:BuildJob和UnitTestJob。
|
|
|
|
|
|
|
|
|
|
按照Job模型的规则,子Job都运行成功,父Job才能成功。所以,BuildJob和UnitTestJob运行成功,DevJob才算成功,而DevJob成功了,后面的TestJob才能触发运行。
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/26/cd/26d9e9dd80fbc153542b8553b28874cd.jpg?wh=1920x1038)
|
|
|
|
|
|
|
|
|
|
### TestJob
|
|
|
|
|
|
|
|
|
|
经过第二模块里策略篇的学习,我们知道了测试的策略有单元测试、接口测试和系统测试。单元测试一般是开发人员来做,已经包含在DevJob里了。而接口测试和系统测试,就需要放在TestJob里。
|
|
|
|
|
|
|
|
|
|
先来看APITestJob的模型。APITestJob作为TestJob的第一个子Job,APITestJob的Input就是它的父Job TestJob的Input,也就是DevPackageUrl。有了这个信息,APITestJob就可以开发实现了,它的自动化工具可以选用RestAssure。
|
|
|
|
|
|
|
|
|
|
然后是E2ETestJob的模型,它的Dependency是APITestJob,而Output是ReleasePackageUrl。至于E2ETestJob的自动化技术,我们可以选用Selenium。
|
|
|
|
|
|
|
|
|
|
现在,可以把TestJob分解成了2个子Job,APTestJob和E2ETestJob,Job树如下:
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/de/89/dede1f23e6df7yyfb231e2b5b2702f89.jpg?wh=1920x1038)
|
|
|
|
|
|
|
|
|
|
### DeploymentJob
|
|
|
|
|
|
|
|
|
|
又下一城,咱们一鼓作气,继续来看DeploymentJob。
|
|
|
|
|
|
|
|
|
|
当Pipeline移动到DeploymentJob的时候,它会检查前置依赖TestJob的状态,只有成功了,DeploymentJob才会运行。在运行的时候,DeploymentJob会从Input获得一个ReleasePackageUrl,然后经过一系列部署操作,把Package部署到生产环境里开始运行,输出一个serviceUrl。
|
|
|
|
|
|
|
|
|
|
为了完成这个过程,简单的做法是,DeploymentJob不需要再细分子Job,你可以直接把它当作一个可执行Job,用部署脚本来实现。
|
|
|
|
|
|
|
|
|
|
## 转成自动化测试开发计划
|
|
|
|
|
|
|
|
|
|
搞定上面的设计后,现在Job树变成了这样:
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/51/8b/51a0de00fc0c6e5e8e76e98a3b1b528b.jpg?wh=1920x1087)
|
|
|
|
|
|
|
|
|
|
现在每一个叶子结点,就是可执行的Job,需要自动化实现。
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/74/78/741517d2175c0ee8a4e648e811638e78.jpg?wh=1920x693)
|
|
|
|
|
|
|
|
|
|
## 变更和扩展
|
|
|
|
|
|
|
|
|
|
在Pipeline搭建起来后,我们还要持续维护。这里有两种情形,一种是新增Job,还有一种是扩展Job。
|
|
|
|
|
|
|
|
|
|
先看新增Job的场景,我给你举个例子。比如开发人员有一天要引入Sonar,做代码质量检测,那我们该怎么做呢?
|
|
|
|
|
|
|
|
|
|
动手前,我们还是先分析一下。Sonar扫描,也是基于代码CodeRepoUrl,这个和DevJob的Input一致。这样,我们可以建立一个SonarJob,作为DevJob的子Job,可以复用DevJob的Input。
|
|
|
|
|
|
|
|
|
|
那SonarJob和BuildJob,还有UnitTestJob是什么关系呢?这就看我们想怎么定义它们的依赖关系了。也就是谁在前端,谁在末端。
|
|
|
|
|
|
|
|
|
|
一般来说,代码扫描,应该发生在代码变更的最早时刻。一旦有代码提交,就首先检查代码的质量,如果代码质量不符合预定标准,就终止Pipeline。实践里这样选择,是为了优先保证代码的质量。
|
|
|
|
|
|
|
|
|
|
基于这样的目标,我们就把SonarJob添加到DevJob下的第一个子Job,让BuildJob依赖SonarJob,而UnitTestJob保持不变,还是依赖BuildJob。
|
|
|
|
|
|
|
|
|
|
另外还有一种情形,就是某一个Job当时设计实现比较简单,后来需要扩展。比如DeploymentJob,刚开始我们用一段Shell脚本就可以轻松执行它。但随着Deployment要求提高,我们需要在Deployment结束后,还要执行测试任务,验证质量,然后再发布serviceUrl。面对这种情况,我们该怎么办?
|
|
|
|
|
|
|
|
|
|
其实这个场景,就跟开发一样,一旦代码规模扩大了,就需要分拆模块了。
|
|
|
|
|
|
|
|
|
|
在我们的Job树里,原先的DeploymentJob需要分拆了,拆分成ExecuteDeployment和TestDeployment,让TestDeployment依赖于ExecuteDeployment。之前的DeploymentJob变成一个抽象Job,它的脚本实现挪到ExecuteDeploymentJob里,在新TestDeployment里,加上测试任务即可。如下图:
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/8b/96/8bc413d8d91f669927efde462fc77d96.jpg?wh=1920x1268)
|
|
|
|
|
|
|
|
|
|
## 小结
|
|
|
|
|
|
|
|
|
|
为了检验咱们的Job模型威力几何,今天我们再次锻炼设计思维,进行了部署Pipeline的设计。经过案例演练,你现在是不是对Job模型更加熟悉了呢?
|
|
|
|
|
|
|
|
|
|
咱们回顾一下关键流程。概要设计里,我们完成了抽象Job的定义,对应着Pipeline划分的三大阶段:开发阶段、测试阶段和部署阶段,然后逐层细化,一直到实例Job。而在详细设计里,我们不仅要确定实例Job的接口,而且还要确定它们的自动化测试实现技术。
|
|
|
|
|
|
|
|
|
|
做完设计,每一个叶子节点的Job都是要去开发实现的自动化测试任务。我们就可以整理出一个自动化测试开发任务列表了。
|
|
|
|
|
|
|
|
|
|
后面是Job树叶子工作表,供你复习回顾。
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/74/78/741517d2175c0ee8a4e648e811638e78.jpg?wh=1920x693)
|
|
|
|
|
|
|
|
|
|
一个优秀模型,不光着眼于眼前的需求,还具备应对未来变化的能力。除了概要设计和详细设计,我们还推演了未来的应变策略。
|
|
|
|
|
|
|
|
|
|
当规模扩大时,原先的可执行Job的逻辑变得复杂的时候,我们依然可以根据Job模型,把可执行Job再进行分解成多个子Job。需要注意的是,我们遵循的原则是只有叶子结点Job才是可执行Job,不再进行分解的Job,而一旦Job下挂了子Job,那这个Job就变成了抽象Job,只负责接口的定义。
|
|
|
|
|
|
|
|
|
|
今天讲了Pipeline设计,也是对前面Job设计内容的复习巩固。接下来,我们就要进阶到另外一个难度级别的设计了,不仅有多端协作,还有分布式事务,这种场景怎么设计?我们下讲见。
|
|
|
|
|
|
|
|
|
|
## 思考题
|
|
|
|
|
|
|
|
|
|
你负责或者参与的Pipeline原先是怎么设计的?看完这一讲,你想到怎么优化它了么?
|
|
|
|
|
|
|
|
|
|
欢迎你在留言区跟我交流互动。如果觉得这一讲对你有启发,也推荐你把它分享给更多朋友、同事。
|
|
|
|
|
|