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