@liuhui0803
2016-09-02T09:45:53.000000Z
字数 7290
阅读 2230
Netflix
测试
体系结构
自动化
摘要:
本文是系列文章中的第1篇,主要介绍了Netflix在不同设备上针对Netflix SDK进行自动化的功能、性能,以及压力测试过程中所涉及的重要概念和所使用的基础架构。
正文:
作为Netflix SDK团队的一份子,我们需要负责确保新版Netflix应用程序经历彻底的测试,以最高运维质量部署到游戏主机平台,并以SDK(以及参考应用程序)的方式交付给Netflix设备合作伙伴,最终发布到数百万智能电视和机顶盒中。总的来说,我们的测试需要确保Netflix能够在数百万游戏主机和网络电视/机顶盒中流畅运行。
与服务器端的软件发布不同,针对这类设备发布会面临一些独特挑战,因为无法进行Red/black推送,出现故障也无法立刻回滚。在将代码发布到客户端之后,如果客户端存在Bug,修复成本将非常高。Netflix必须重新召集已获得Netflix认证的设备合作伙伴,并在Bug修复后重新进行整个认证过程以进行确认,这需要我们与合作伙伴公司重新投入大量工程时间。而在这个过程中,客户可能无法解决自己所遇到的问题,只能暂时忍受不甚理想的Netflix使用体验。为了避免出现这种问题,最适合的做法是确保对设备进行全面测试,在最终发布前找出应用程序中可能存在的问题。
本文是系列文章中的第1篇,主要介绍了我们在不同设备上针对Netflix SDK进行自动化的功能、性能,以及压力测试过程中所涉及的重要概念和所使用的基础架构。
过去多年来,我们在Netflix应用程序的测试过程中同时使用了手工和自动化的方式,并获得了不少经验教训。因此在重新设计自动化系统,以便迈上一个新台阶并实现更大规模时,我们也将这些经验教训视作自己的核心目标。
使用自动化方法时,测试的创建和/或使用过程应该更为简单。尤其是原本就很简单的手工测试,使用自动化方式后也必须保持简单。这意味着自动化技术的运用,就算无法完全省略配置成本,也应让成本接近于零。因此我们必须确保新测试的创建和现有测试的调试过程足够快速便捷,同时这也确保了能尽量只关注于测试和功能本身。
使用自动化的系统不应对测试的具体形式产生限制,为了能在未来应用更为创新的测试,这一点至关重要。此外为了更好地满足不同团队(我们主要与负责平台、安全、播放/媒体/UI等的团队打交道)的需求,可能会通过不同方式设计自己的测试。将自动化系统与测试结构解耦有助于提高整个测试的可重用性。
在构建大规模系统时,很容易最终创建出大量抽象层。虽然从根本上来看,大部分情况下这样的结果并没什么不好,但为了将这些层面与自动化系统集成在一起,还需要对这些层面本身进行测试,这并不是我们希望的。实际上除了真正要测试的功能外,需要测试的其他内容越多,遇到问题后的调试就越困难:应用程序之外有那么多东西需要测试,很容易在测试中造成错误。
在我们的例子中,需要测试设备上运行的Netflix,因此要确保设备上所进行的测试对不同功能的调用能够与被测试的SDK功能尽可能保持接近。
手工方法的测试中,设备管理工作花掉了大量时间,因此这一领域很适合使用自动化系统。由于我们测试的都是正在开发中的产品,需要能随时更改构建版本并将其部署到设备上。为了尽量简化测试过程中所遇到错误后的调试过程,还需要自动实现日志文件和崩溃转储文件的自动化提取。
确立了这些目标后,毫无疑问我们的团队需要一种能提供必要自动化机制与设备服务,同时尽量不对测试过程产生干扰的系统。
这就需要重新思考现有的框架并创建出一个全新的自动化生态系统。为了通过自动化获得所需灵活性,需要让这个自动化系统足够精益,采用模块化设计,并且仅在功能测试非常必要的时候才使用外部服务,也就是说只有在功能无法直接通过设备上的应用程序实现(例如暂挂应用程序或操作网络)时才使用外部服务。
将所用外部服务数量降至最低还能提供下列收益:
从最简单的层面来看,我们需要有两套相互独立的实体:
测试框架
通过软件抽象揭示测试流程中需要控制的功能,有助于编写测试案例。
测试框架意在帮助编写测试,为了减少对测试故障进行调试时需要检查的活动部件数量,应尽可能与要测试的设备/应用程序保持密切相关。
活动部件有很多,不同团队可以根据具体需求以相应的方式构建自己的测试。
自动化服务
一套外部的后端服务,可以帮助管理设备,自动执行测试,并在必要时提供测试所需的外部功能。自动化服务应该尽量以自成体系的独立方式构建。减少不同服务之间技术层的数量有助于实现更好的可重用性、可维护性,以及更简单的调试和后续完善。例如对测试的启动过程提供帮助的服务,收集测试运行过程中信息的服务,验证测试结果的服务,都可以委派给不同的微服务实现。这些微服务可以对测试的独立进行提供帮助,但不是运行测试所必须的。自动化服务只应该用于提供服务,不能用于控制测试流。
例如在测试过程中,测试可能要求通过外部服务重启动设备。但这些服务不能用于重启设备并对测试本身进行控制。
在设计自动化服务过程中,我们仔细研究了对这些服务的具体需求。
设备管理
虽然测试本身是自动实现的,但针对不同类型的设备构建测试需要进行大量自定义操作,例如刷机、升级,然后启动应用程序开始进行测试,并在测试结束后收集日志和崩溃转储数据。不同设备上每个此类操作可能都各不相同。我们需要通过服务将不同设备的具体信息抽象出来,并为不同设备提供一个通用接口。
测试管理
测试本身的编写只是这个工作中的一小部分内容,还需要考虑下列这些问题:
网络操控
为确保不间断提供高质量的播放体验,在带宽状况波动的设备上对Netflix应用程序进行测试成了我们的一个核心要求。我们需要通过一种服务更改网络环境,包括流量塑形以及DNS控制。
文件服务
在出于归档目的收集不同构建版本,或需要存储海量日志文件时,我们需要能通过某种方式存储并获取这些文件,为此实施了一套文件服务。
测试运行器(Runner)
由于每个服务是相对独立的,因此我们需要通过某种调度编排程序与不同服务进行通信,以便在测试开始前让设备做好准备,并在测试结束后收集结果。
考虑到上述设计选择,我们构建了下面这套自动化系统。
上述服务通过进一步完善即可满足我们的需求,并且各种服务尽可能保持独立,并为与测试框架进行捆绑。这些概念是按照下列方式执行的。
设备服务可对测试自始至终全程管理设备所需的技术细节进行抽象。通过对所有类型的设备提供一个简单、统一的RESTful接口,无须对特定设备有具体了解即可直接通过该服务使用不同的设备:可以像对待完全相同的设备那样直接使用全部或任何一种设备。
管理每类设备所需的逻辑并非直接在设备服务自身中直接实现的,而是会委派给名为Device handler的独立微服务。
这样既可灵活增添对新类型设备的支持,因为Device handler可以通过任何编程语言用相应的REST API编写,现有Handler也可以轻松集成到设备服务中。一些Handler有时候可能需要与设备建立物理连接,因此将设备服务与Device handler解耦即可忽略设备位置获得更大灵活性。
对于收到的每个请求,设备服务将负责确定要联系的Device handler,并在针对所使用的Device handler接口进行适配后,以代理的方式将请求发送给不同Handler。
一起用一个具体的例子看看… 举例来说,为PS4安装某一构建版本的操作与为Roku安装的过程就有很大不同。前者(PlayStation)需要使用C#编写的代码与Windows平台上的ProDG Target Manager进行交互,后者则需要在Linux上运行使用Node.js编写的代码。PS4和Roku的Device handler分别实施了特定于具体设备的安装程序。
如果设备服务需要与某个设备通信,必须首先知道该设备的具体信息。每个设备都有自己的唯一标识符,设备服务将其以设备映射对象(Map object)的形式存储和访问,其中包含了Handler所需的设备信息,例如:
在将某个设备首次加入自动化系统时,需要填写设备映射信息。
当需要对某个新类型设备进行测试时,要针对该设备实现一个专门的Handler,并通过设备服务暴露。设备服务支持下列常用的设备方法:
- | - |
---|---|
POST /device/install | 安装Netflix应用程序 |
POST /device/start | 使用一系列特定启动参数启动Netflix应用程序 |
POST /device/stop | 停止Netflix应用程序 |
POST /device/restart | 重启动Netflix应用程序(其实就是停止 + 启动) |
POST /device/powercycle | 让设备完成一个通电周期。可直接进行或通过远程电源装置进行 |
GET /device/status | 获取有关设备的信息(例如运行中、已停止等…) |
GET /device/crash | 收集Netflix应用程序崩溃报告 |
GET /device/screenshot | 针对活动界面抓取全屏截图 |
GET /device/debug | 收集设备生成的调试文件 |
请注意,针对上述每个端点发送请求时都需要提供一个唯一标识符。这个标识符(类似于序列号)会和要操作的设备绑定。
保持这套服务尽量简单,也能尽量提高其扩展性。我们可以轻松地为不同设备增加其他能力,如果某个设备不能支持这些能力,将其视作空指令(NOOP)即可。
设备服务还可以用作设备池:
- | - |
---|---|
POST /device/reserve | 预留某个设备并租用一段时间 |
PUT /device/reserve | 续订之前预留设备的租约 |
GET /device/reserve | 列出当前预留的所有设备 |
POST /device/release | 释放原本预留的设备 |
POST /device/disable | 将不允许使用的设备加入临时黑名单(例如设备不可用或运行状况异常时) |
GET /device/disable | 列出当前被禁用的设备 |
下图是我们在实验室中进行自动化测试时用到的部分设备。请留意Xbox 360电源按钮旁边添加的手动机械开关。这是我们为Xbox 360量身打造的自定义解决方案。该设备需要手工按下按钮才能重启动,我们决定设计一套通过树莓派(Raspberry Pi)连接的机械臂让这一过程实现自动化,通过发送信号即可让机械臂按下电源按钮。这一操作已添加至Xbox 360的Device handler中。设备服务的电源周期(Powercycle)端点可以调用Xbox 360的电源周期Handler。PS3和PS4无须这一操作,因此未将其实施到它们的Handler中。
测试服务负责对测试案例的运行进行记录。其用途在于标记测试案例的开始,并在测试结束前持续记录状态的变化,用日志保存相关信息、元数据、文件链接(测试过程中收集的日志/小型崩溃转储文件),以及测试案例所生成的所有数据序列。该服务暴露的简单端点可被运行测试案例所需的测试框架所引用:
- | - |
---|---|
POST /tests/start | 将测试标记为已启动 |
POST /tests/end | 将测试标记为已结束 |
POST /tests/configuration | 发布版本、设备型号等设备配置信息 |
POST /tests/keepalive | 设备不响应后进行TTL运行状况检查 |
/tests/details | 发布测试数据/结果 |
测试框架内部通常会调用下列端点:
为了与设备通信,以及进行流量塑形和DNS控制,我们开发了一个名为Bifröst Bridge的网络系统。我们并没有更改网络拓扑,而是直接将设备连接至主网络。Bifröst Bridge并非运行测试所必须的,只有测试需要对网络进行操控,例如更改DNS记录时才会可选使用。
运行测试的过程中,我们可以收集测试生成的文件并通过文件服务将其上传至存储仓库。收集的内容包括设备日志文件、崩溃报告、屏幕截图等。从面向消费者的客户端角度来看,这个服务的使用非常简单:
- | - |
---|---|
POST /file | 不指定名称的情况下上传文件,这样最终文件中会包含一个唯一标识符,并可通过该标识符下载文件 |
GET /file/:id | 下载包含特定标识符的文件 |
文件服务基于云存储平台,为了快速检索,还通过Varnish Cache对资源创建了缓存。
我们选择使用MongoDB作为测试服务所用的数据库,因为这种数据库可支持JSON格式和“无架构(Schema-less)特性。通过开放式JSON文档存储解决方案获得的灵活性是这套系统的关键需求,因为测试结果和元数据的存储会不断变化,不应受到结构的限制。虽然从数据库管理的角度来看,使用关系性数据库也很合理,但我们以“即插即用”为基本原则,因此无论测试需求如何,数据库的架构都必须用手工的方式保持最新。
通过使用CI模式运行,每次测试可以记录一个唯一的运行ID,并借此收集有关构建配置、设备配置、测试详情等信息。日志文件在文件服务中的下载链接也会存储在数据库的测试项中。
为了减轻每个测试发起人分别使用不同服务运行不同测试所面临的负担,我们开发了一个名为Maze Runner的控制器,该控制器可以对测试的运行进行编排,并按需调用不同的服务。
测试套件的所有者可以通过创建脚本指定用于运行测试的设备(或设备类型),并结合测试套件的名称和测试案例组成一个测试套件,随后由Maze Runner(并行)执行该测试。
Maze Runner可执行的操作如下:
测试框架完全独立于自动化服务,因为测试框架只是用于在设备上运行测试所用。大部分测试可以无须自动化服务手工运行,而这也是设计这套系统时的一个核心原则。这种情况下可以手工启动测试,并在测试完成后的手工获得并查阅结果。
然而测试框架也可以配合自动化服务使用(例如用测试服务存储测试进度和结果)。如果通过运行器在CI中运行测试,我们需要将其与自动化服务相集成。
为了用灵活的方式实现这一目标,我们创建了一个在内部被称之为TPL(Test Portability Layer)的抽象层。测试和测试框架通过调用这个抽象层可以为每个自动化服务定义一个简单的接口。每个自动化服务可提供这些接口所需的实现。
针对该系统所实现的服务,这个抽象层可以让原本需要自动运行的测试能够通过TPL接口提供的截然不同的自动化系统来运行。这样就可以让其他团队(使用其他自动化系统)编写的测试案例不经改动直接运行。如果测试无须改动,设备上测试运行失败后就无须由测试的所有者进行排错,这正是我们希望实现的。
通过让测试框架与自动化服务保持独立,按需使用自动化服务并增添所需的设备功能,我们实现了:
从最新的测试执行覆盖图中可以看到,仅针对参考应用程序,每个构建版本已执行了大概1500次测试。从全局的角度来看,开发团队每天会为每个分支生成大约10-15个构建版本,每个版本包含5个不同用途(例如调试、发布、AddressSanitizer等)的参考应用程序。对于游戏主机,每天每个用途会生成大约3-4个构建版本。总的来说,针对单一用途的构建版本,我们的生态系统每天可以运行大约1500*10 + 1500*3,即大约2万个测试案例。
考虑到每天要运行大量测试,我们又遇到了两个重要的挑战:
在后续文章中,我们将深入介绍为了解决上述两大挑战,目前我们所采取的一系列创新措施。
作者:Benoit Fontaine、Janaki Ramachandran、Tim Kaddoura、Gustavo Branco,阅读英文原文:Automated testing on devices