本质上这是一种“按心情”执行。一般需要开发者在本地开发完毕之后,自觉执行脚本并观察端到端测试结果。“按心情”的事情,自然是无法正规化、流程化、平台化的,注定是比较鸡肋的存在。
顺着上述思路,我们可以想到:借助 huskey,将上述端到端测试放到 pre-commit 或者 pre-push 阶段强制执行。这样一来我们使得端到端测试流程化,将“按心情”执行改为了强制执行。
但进一步思考,在 pre-commit/pre-push 阶段执行的弊端也很明显:增加了额外的 git hooks,延长了代码提交流程,直接影响到了上线效率。如果是一个 hot-fix 紧急修改 bug,这样的时间殇是我们无法接受的。同时,本地阶段执行端到端测试的一个前提就是先要保证本地服务的可用性,暂且不谈开启本地服务的时间开销,一个更尴尬问题就在于:本地服务和线上环境是有天然差别的,本地执行的端到端测试难以幂等于线上真实效果。
基于上述情况,端到端测试在团队技术体系中,要么渐渐地成为一个“漂亮的玩具”,华而不实;要么就是一个开发者“眼见心烦的累赘”,最终沦为鸡肋。
这么看来,端到端测试想要破局并突破,势必应该在执行流程上进行创新。为此,我们认为:端到端测试应该搬到“容器”上进行,融合到 CI/CD 阶段实施,彻底做到自动化、服务化。
这里插入一个知识点:什么是 CI/CD 阶段呢?
它们都是现代互联网应用编译和发布流程当中的常用词语,分别对应:持续集成(Continuous Integration)和持续部署(Continuous Deployment),实际上我们还有一个持续交付(Continuous Delivery)的概念,这里不再详细展开,我们来聚焦到持续集成和部署。
在持续集成环境中,开发人员将代码提交到主干 master,并触发 Gitlab 的 hook,进而自动推进代码的编译。不同的团队对持续集成阶段的定义也许会有略微不同,但是并不妨碍我们理解。我司在持续集成阶段主要完成:构建项目流程。具体来说,在这个阶段中台团队使用基础镜像启动容器,拉取最新代码,安装必要依赖,执行单测脚本,并最终 commit 出下一阶段(持续部署阶段)的镜像。
在持续部署阶段,中台使用构建阶段(持续集成阶段)产出的镜像,启动容器,按照一定 pipeline 流程,串行发布最新版本应用,最终启动服务。
了解 CI/CD 的概念之后,将端到端测试后置到 CD/CD 阶段,并在真实容器中执行——这似乎是一个很好的尝试和创新。
截止为此,根据我们的“容器中执行端到端测试服务并接入 CI/CD”的设计思想,我们可以画出来一个简单的流程分析。
由上图,衍生出第一个问题:我们应该将端到端服务接入到 CI 阶段还是 CD 阶段呢?
按照常理:
CI 阶段应该重视测试验证结果,以保障所有的提交在部署前的质量,对可能出现的一些问题进行预警;
CD 阶段应该没有人为干预,只有当一个修改在工作流 pipeline 中构建失败才能阻止它部署到产品线。
但是,端到端测试需要保证具有一个可访问的最新版本应用地址,而 CI 阶段在我司只是进行代码的编译和容器基础镜像的生成,并不会开启应用服务,因而不具备端到端测试所需的地址。我们当然可以“改造” CI 阶段,新启动一个新的进程,进行应用服务的启动,但这样的做法显然粗暴而不合理。
时 CD 阶段我司在金丝雀过程之前,有一个“办公室阶段”(下文统一使用办公室阶段/办公室环境一词),即:办公室内(公司内网环境下)可全量访问新版本应用。也就是说公司内网下访问线上地址:www.a.com/b,网关会将该流量全部…
综合考虑,对于我司来讲,在这个“办公室”阶段,应该是最好执行端到端测试的时间点。一旦端到端测试无法通过,将会中断部署交付流程。
这样一来,就解决了全量可测页面的访问性问题,同时端到端测试的环境完全和线上环境保持一致。
任何一个创新型项目的开展之路,都注定曲折坎坷。设计先行,但在实施过程中,我们还是遇见了较多的阻力和难点。主要问题集中在端到端框架和中台容器的贴合性、一致性上。下面我举一些典型例子来进行说明。
我们选用了目前业界最为流行和活跃的 Cypress 作为端到端测试框架,关于不同端到端框架的对比和技术实现原理这里不再赘述,感兴趣的同学可以关注我们的博文,后续将会专门进行解剖。
整体端到端服务流程并不复杂,如下图:
这只是一个极简的图示,粗略地表现了在相关代码 MR(merge request)成功构建,并部署到办公室环境时,中台请求我们的端到端测试服务开启接口。
这就存在了第一个难题:我们发现,在办公室部署完成之后,端到端服务接收到 post 请求,执行 cypress.run(),总会得到报错:Cypress binary is not installed。为什么本地就能顺利执行,到了容器上,开启 NodeJS 服务之后就会得到报错呢?
翻看 Cypress 代码实现,究其原因非常有趣,Cypress 会在 npm post-install 过程安装 Cypress binary 到容器系统路径下,post-install 是 npm 的一个 hook,它会在 npm install 成功执行后触发。在执行 cypress.run() 时,Cypress 会先执行 cypress.verify() 验证 Cypress 的可用性,其中一个验证标准就是检查系统路径下是否存在 Cypress binary。
那为什么我们容器上就找不到 Cypress binary 呢?我依然用图示还原案发现场:
在第一次构建时,我们的构建脚本执行 npm install,并成功触发 npm post-install,Cypress 将 Cypress binary 安装在容器系统路径:~/.cache/Cypress 当中。
第二(N)次构建时,面对一个“全新”的空容器,中台为我们缓存了 node_modules,因此 npm install 并不会真正下载依赖,post-install 的 hook 也不会触发,也就不存在“Cypress 将 Cypress binary 安装在容器系统路径:~/.cache/Cypress 当中”这一动作。进而执行时,得到了 Cypress binary is not installed 的报错。
解决方案也不难,我的第一个直观想法是构建脚本当中的 npm install 改为 npm ci, 这里插入一下 npm ci 和 npm install 的区别:
npm ci 需要项目必须要含有 package-lock.json 或者 npm-shrinkwrap.json 文件
如果上述两种 lock 文件和 package.json 声明的依赖产生冲突,npm ci 命令会强行退出,并报错,而 npm install 命令会更新 lock 文件
npm ci 命令会全量安装项目所有的依赖,无法添加单独依赖项目
如果项目中已经存在 nodemodules,npm ci 命令会删除 nodemodules 文件,并重新安装
npm ci 命令不会写 package.json 内容以及 lock 文件内容
因此不难看出,在构建阶段,本就应该使用 npm ci 命令,这也是 npm ci 命令命名的由来。
但使用 npm ci 和中台缓存 node_modules 的行为又相矛盾,无可避免地增加了构建的耗时。在任何公司的构建部署系统中,npm ci 安装依赖的时间一定会是不可忽略的大头之一。
有没有更“优雅”的方法呢?我坚信“ PR makes world better”。让我们回到本质,核心问题在于「中台缓存了 nodemodules,导致 post-install 无法触发,进而无法安装 Cypress binary 到容器系统路径」,如果我们也能缓存 Cypress binary 到指定路径,且在执行 cypress.run() 以及 cypress.verify() 时,让 Cypress 去设定的缓存路径下找 Cypress binary 是不是就能解决问题。那么这个”缓存路径“当然就是 nodemodules 文件下的某个路径即可(因为中台缓存了 node_modules 文件)。
总结一下,关键点在于:
Cypress 需要新增可配置环境变量,用于指明 Cypress binary 的安装路径
我们设置环境变量 CYPRESSCACHEFOLDER 为 ./node_modules/.cache/cypress/
cypress.run() 触发 cypress.verify() 执行时,去 CYPRESSCACHEFOLDER 指定的路径下查找 Cypress binary 是否存在
此时整体流程如图:
对于增加配置环境变量,使得容器环境执行 Cypress 更加灵活的提议,当然也得到了 Cypress 官方的认可,此问题暂告解决。同时,对于 Cypress 本身体积较大,安装耗时且不稳定的问题,我们同样使用一个 CYPRESSINSTALLBINARY 环境变量指明默认的 Cypress 软件下载地址。我们在公司内网保存一份,内网下载 Cypress 既迅速又可靠。最终的构建部分脚本如下 (采用 yml 格式,不影响读者理解):
build: # export cypress variables - export CYPRESS_CACHE_FOLDER=node_modules/.cache/Cypress&& export CYPRESS_INSTALL_BINARY=http://内网地址/cypress.zip ## application build - yarn - yarn build
其中可见在执行依赖安装(yarn)和构建项目(yarn build)之前,我们声明并导出了相关环境变量。
解决了 Cypress binary 安装问题,我们在执行过程中遇到的第二个问题也很有趣。在 cypress.run() 执行时,得到报错信息,“CI stage dependency missing in docker”,经过和官方团队的讨论:
我们严重怀疑容器上执行 Cypress 出错的原因在于:容器系统版本过低。中台当前提供的容器系统版本均为:Debian 8.2(jessie),NodeJS v10.14.0,即 docker 基础镜像声明为:baseimage: nodejs/v10.14.0jessie (debian 8)。为此,我们组织中台团队以及公司内部安全组进行沟通,并制作出加入了安全包的新版本 baseimage: nodejs/v12.13.0stretch (debian 9)的基础镜像,供项目容器使用。
基础镜像的升级并不是简单制作一个镜像那么简单,其中涉及到较多“技术之外”的探索和磨合,这里我们不过多展开。总之,中台团队的存在对于各种类型 NodeJS 应用/服务的落地和发展至关重要。同时中台方面涉及到的能力是传统前端开发所欠缺的。因此,项目推动能力,跨团队沟通能力也是 NodeJS 发展甚至任何一项前端技术都不可忽视的一环。
此外 Cypress 作为一个复杂的端到端测试框架,它本身需要很多系统级的依赖,比如 Xvfb(is an X server that can run on machines with no display hardware and no physical input devices/虚拟屏幕虚拟输入设备)等,这里梳理总结一下必备系统依赖包包括:
xvfb
libgtk-3-dev
libnss3
libxss1
libasound2
xz-utils
到此为止,简要总结一下“端到端测试上容器”这一过程遇见的关键问题以及解决方案:
Cypress binary 安装问题:提 PR 解决,提供 Cypress binary 缓存安装路径
Cypress 安装超时且不稳定:提 PR 解决,提供内网获取路径,从内网下载
容器系统不兼容:制作镜像并推动中台升级容器系统基础镜像
当然以上问题并不是全部,但极具代表性,也能总结出任何一个前端团队在公司内推广落地新技术时可能会遇见的问题。具体的挫折可能来自 NodeJS 服务自身,也可能来自于和已有技术体系的不兼容,解决方案有技术方向的努力,也有项目推动方向的尝试。
到此,我们涉及了服务粗略设计以及基础环境的搭建。接下来,我将重点介绍一下容器化端到端测试服务的技术体系架构设计。
文章主题围绕着如何开发一个“容器上运行的端到端测试系统”展开,前面也提到过,其实就是在合适的时机去触发端到端框架的执行,想来就这么简单。但是我们在设计一个系统,一个平台时,应该考虑更多问题,比如:
横向多项目扩展能力
平台化服务能力
运行效率极致化设计方案
通报与预警中断机制
合理选型技术方案以及存储方案
我们的端到端服务起名为「Goalkeeper」,意为“守门员”,希望它像一名优秀的守门员一样,守卫着我们产品质量的最后一道防线。
Goalkeeper 目前已经进入成熟阶段,从立项的角度来说,该 NodeJS 服务不能只服务于一个项目测试,理想地它应该具备支持公司内所有产品接入的能力,并将接入过程和复杂度降到最低。
当办公室环境部署完成后,中台请求 Goalkeeper Post 接口 https://api.goalkeeper.com/run,这个接口提交数据字段包括:
{ "stage_name": "office", "description": "style: 1221 活动页样式兼容低版本安卓", "mr_iid": 2049999, "app_name": "xen", "author": "houce", "event_name": "deployment_finished", "candidate_id": 6666, "deploy_id": 6666 }
app_name 字段为唯一的项目名称,配合其他表意字段(应该不难理解,这里不再一一说明),这样的接口设计自然支持全公司所用应用的接入,仅从接口设计上,具备先天扩展能力。接下来的说明也将进一步就横向扩展来展开。
为了最高效地进行端到端测试,我们分析:对于不同的应用,应该多核多进程执行端到端测试,保证不同应用测试任务执行的并行性,即对于多个项目的部署,端到端测试不会发生阻塞,不排队;对于同一个项目应用,必须要避免短时间内多次不同部署之间的互相影响,对于这些端到端测试执行任务应该正交化设计,串行展开。
具体实施就需要一个消息队列,不同应用采用不同消息队列 tube,相同应用在同一个 tube 中串行生产和消费。因为 Goalkeeper 是一个服务内的消息队列设计,因此我选用了轻量且兼具强大功能的 Beastalkd 作为消息队列的技术选型。
具备了支持多应用的能力,接下来很自然地就想到:「开发者如何查看测试报告和了解测试细节呢」?
Goalkeeper 的设计包含了非常重要的一块内容 —— 平台化展示。这其实就是一个典型的:基于 Koa 的 NodeJS 后端服务,前端采用 React 作为多页面应用方案。具体来说,每次端到端测试服务完成之后,将产生的所有测试报告类数据存入 Redis,开发者访问 https://www.goalkeeper.com/dashboard,Koa 基于服务端渲染,获取相关数据进行单页面应用的平台化展示。
这些相关数据,不仅包含了每个端到端测试的 case 内容、case 执行信息和结果,还包含了测试产生的富媒体文件地址(包括测试录像,测试截图等)。关于测试产生的富媒体文件,我们采用了容器持久化技术进行存储,并对外提供静态服务。
换句话说,Goalkeeper 在 NodeJS 的服务层面提供了:
容器上的端到端测试
整套单页应用服务(包括查询平台和富媒体静态服务等)
为了更好地服务线上应用,我们也设计了高效的通报与预警机制。通报机制是指在一次提交部署开始,对应相关的端到端测试完成之后,通过企业微信和邮件将测试信息和测试平台展示地址发送给提交人或负责人。预警中断机制是指在端到端测试发现异常结果时,阻断上线流程,并强通知给提交人和负责人。
我们的异常结果不仅包含测试 case 的失败,更具特色的是也包含了视觉比对测试(visual testing)的异常。基于 Cypress,我们封装了一套视觉测试插件,它能够在任意节点自动对测试页面进行全量截图,并保存为对比基准图片。在下一次测试进行时,对当前最新测试的相同节点进行页面全量截图,并进行和基准图片的比对,如果两幅图片的不同像素超过一定百分比或一定像素阈值,则认为视觉比对失败。
我们通过分析一个请求的流程,再来总结梳理一下整个设计过程:
更细节一点的图示例:
开发者提交代码被合并,Merge Request id 为 123 的相关部署到内网环境之后,触发中台 hook,中台会请求端到端测试 Goalkeeper 服务接口 ./run,该服务会为每一个应用创建一个进程处理,利用消息队列机制跑该次部署的端到端测试,并最后将测试状态结果(running/success/fail)写入 Redis 当中。在这个过程中,中台可以根据轮询接口 ./consult,该接口查询 Redis 中相关 Merge Request id 的测试状态,中台根据该结果值进行解锁上线流程或继续锁定上线。
同时,在该次部署所对应的端到端测试结束时,会更新测试报告平台内容,方便开发者访问最新部署产生的端到端测试报告以及录像等富媒体信息。相应的通知和预警机制也会在该阶段触发。
整套系统的关键依赖的服务项如图:
整个 Goalkeeper 平台主要依靠 Koa,Koa-static,Koa-router 来处理测试服务请求,并提供可查询的测试报告平台后台服务。Cypress 是主要的端到端测试框架,它提供了丰富的插件和扩展能力,我们在 Cypress 的基础上,封装了大量贴合自己业务的插件和扩展,比如实现视觉比对测试的 @kfe/goalkeeper-image-snapshot,@kfe/goalkeeper-image-snapshot-runner。Cypress 对应的测试脚本 cases 我们用一个单独的 Gitlab 仓库维护,每次在部署发生并启动端到端测试时,拉取最新的测试 cases 代码。最后,@kfe/goalkeeper-report-generator 是整个可查询平台的仓库,它是一个完整的基于 React SSR 的单页面应用,根据 React-router,提供了:
首页,仪表盘页面(/dashboard)该页面展示了所有已接入的应用项目基本信息
应用项目详情页(/:app)该页面展示了当前应用项目的基本信息
项目部署列表页(/:app/mrList)该页面展示了当前应用项目下,所有的部署信息列表
测试详情页(/:app/mrList/:mrId)该页面展示了当前部署对应的测试信息
测试媒体查询页面(/:app/:mrId/media)该页面展示了当前部署对应测试的富媒体信息,包括测试录屏、应用截图等
下一篇:用n管理node版本