Netflix 在移动设备上的自动化测试

乔梁 | 2022-06-16

作为 Netflix SDK 团队的一员,我们的职责是确保 Netflix 应用程序的新发布版本在部署到游戏机上并作为 SDK(连同参考应用程序)分发给 Netflix 设备合作伙伴之前经过全面测试,以达到最高的操作质量; 最终进入数以百万计的智能电视和机顶盒 (STB)。总体而言,我们的测试负责在数百万台游戏机和联网电视/机顶盒上运行的 Netflix 质量。

与服务器端的软件发布不同,设备发布的独特挑战是不能有红/黑推送或在失败的情况下立即回滚。如果客户端中存在错误,则在客户端设备上发布代码后修复问题的成本非常高。Netflix 必须重新与设备可能已经通过 Netflix 认证的各种合作伙伴进行接触,一旦应用修复程序,就会再次启动循环以重新认证设备,这会在外部和内部花费工程时间。一直以来,客户可能没有解决该问题的方法,从而使他们面临次优的 Netflix 体验。避免此问题的最明显方法是确保在设备上进行测试,以便在发布发布之前检测应用程序的回归。

这是一系列文章的第一部分,描述了我们用来在多个设备上对 Netflix SDK 进行功能、性能和压力测试的关键概念和基础设施。

理想目标 多年来,我们使用手动和自动方式测试 Netflix 应用程序的经验教会了我们几个教训。因此,当需要重新设计我们的自动化系统以更上一层楼并扩大规模时,我们确保将它们设置为核心目标。

低设置成本/高测试“敏捷性” 当使用自动化时,测试不应该更难创建和/或使用。特别是易于手动运行的测试应该在自动化中保持简单运行。这意味着使用自动化的设置成本应该接近于零(如果不是的话)。这对于确保创建新测试和调试现有测试既快速又轻松非常重要。这也确保了尽可能长时间地关注测试和测试中的功能。

无测试结构约束 使用自动化系统不应限制测试以特定格式编写。这对于允许未来测试编写方式的创新非常重要。此外,不同的团队(我们与负责平台、安全、播放/媒体/ UI 等的团队互动)可能会提出不同的方法来构建他们的测试,以更好地满足他们的需求。确保自动化系统与测试结构分离可以提高其可重用性。

测试级别的几层 在构建大型系统时,很容易以过多的抽象层告终。虽然在许多情况下这本身并不是坏事,但当这些层也被添加到测试本身以允许它们与自动化集成时,它就会成为一个问题。事实上,您离实际测试的功能越远,出现问题时就越难以调试:被测应用程序之外的更多事情可能会出错。

在我们的案例中,我们在设备上测试 Netflix,因此我们希望确保测试在设备本身上运行,调用尽可能接近被测试 SDK 功能的函数。

支持重要的设备功能 手动完成设备管理会消耗大量时间,因此是良好自动化系统的重要组成部分。由于我们测试的是正在开发的产品,因此我们需要能够动态更改构建并将它们部署到设备上。提取日志文件和故障转储对于自动化也非常重要,以简化调试测试失败的过程。

设计自动化 有了这些目标,很明显我们的团队需要一个系统来提供必要的自动化和设备服务,同时尽可能地避开测试。

这需要重新思考现有框架并创建一种新型自动化生态系统。为了自动化提供这种灵活性,我们需要自动化系统是精简的、模块化的,并且仅在绝对需要测试功能时才需要外部服务,也就是说,只有当功能不能直接从设备上的应用程序完成时(例如暂停应用程序或操纵网络)。

将外部服务的使用降至最低限度有几个好处:

它确保有关测试的逻辑尽可能地驻留在测试本身中。这提高了测试的可读性、可维护性和可调试性。 大多数测试最终没有外部依赖,允许开发人员尝试重现错误来运行测试,而无需使用他们习惯使用的工具进行设置。 测试用例作者可以专注于测试设备的功能而不必担心外部约束。 在最简单的层面上,我们需要有两个独立的实体:

测试框架 一种软件抽象,通过公开负责测试控制流的功能来帮助编写测试用例。

测试框架是关于帮助编写测试的,并且应该尽可能接近被测试的设备/应用程序,以减少调试测试失败时需要检查的移动部件。

它们可能有很多,以便不同的团队可以以符合他们需求的方式构建他们的测试。

自动化服务 一组外部后端服务,帮助管理设备、自动执行测试以及在绝对需要时提供外部测试功能。自动化服务应尽可能以最独立的方式构建。减少服务之间的联系可以实现更好的可重用性、维护、调试和演进。例如,有助于启动测试、收集有关测试运行的信息、验证测试结果的服务可以委托给各个微服务。这些微服务有助于独立运行测试,而不需要运行测试。自动化服务应该只提供服务而不应该控制测试流程。

例如,测试可以要求外部服务重新启动设备作为测试流程的一部分。但服务不应该指示测试重新启动设备并控制测试流程。

构建即插即用生态系统 在设计自动化服务时,我们研究了每项服务的需求。

设备管理 虽然测试本身是自动化的,但在各种设备上进行测试需要一些自定义步骤,例如在测试开始之前刷新、升级和启动应用程序,以及在测试结束后收集日志和故障转储。这些操作中的每一个在每个设备上都可以完全不同。我们需要一种服务来抽象设备特定信息并为不同设备提供通用接口

测试管理 编写测试只是故事的一小部分:还必须注意以下几点:

将它们分组(测试套件) 选择何时运行它们 选择运行它们的配置 存储他们的结果 可视化他们的结果 网络操纵 在带宽波动的设备上测试 Netflix 应用程序体验是确保高质量不间断播放体验的核心要求。我们需要一种可以改变网络状况的服务,包括流量整形和 DNS 操作。

文件服务 当我们开始收集构建以用于归档或存储大量日志文件时,我们需要一种存储和检索这些文件的方法,并且实现了文件服务来帮助实现这一点。

测试赛跑者 每个服务都是完全独立的,我们需要一个协调器来与单独的服务对话,以便在测试运行之前获取和准备设备,并在测试结束后收集结果。

考虑到上述设计选择,我们构建了以下自动化系统。

下面描述的服务不断发展以满足上述特定需求,并遵循尽可能独立且不绑定到测试框架的原则。如下所述,这些概念已付诸实践。

设备服务 设备服务抽象了从头到尾管理设备所需的技术细节。通过为所有类型的设备公开一个简单的统一 RESTful 接口,该服务的消费者不再需要任何特定于设备的知识:他们可以像使用相同的设备一样使用所有和任何设备。

管理每种类型设备的逻辑不是直接在设备服务本身上实现,而是委托给其他独立的称为设备处理程序的微服务。

这带来了灵活性,增加了对新型设备的支持,因为设备处理程序可以使用他们自己选择的 REST API 以任何编程语言编写,并且现有处理程序可以轻松地与设备服务集成。一些处理程序有时还需要与设备的物理连接,因此将设备服务与设备处理程序分离可以灵活地定位它们的位置。

对于收到的每个请求,设备服务的作用是确定要联系哪个设备处理程序,并在将请求适应设备处理程序接口的一组 REST API 后将请求代理给它。

让我们看一个更具体的例子……例如,在 PS4 上安装构建的操作与在 Roku 上安装构建有很大不同。一个依赖于用 C# 编写的代码与在 Windows(用于 PlayStation)上运行的 ProDG 目标管理器接口,另一个是用在 Linux 上运行的 Node.js 编写的。PS4 和 Roku 设备处理程序都实现了自己的设备特定安装过程。

如果设备服务需要与设备通信,它需要知道设备特定的信息。每个设备都有自己的唯一标识符,设备服务作为设备映射对象存储和访问,其中包含有关处理程序所需设备的信息。例如:

设备 IP 或主机名 设备 Mac 地址(可选) 处理程序 IP 或主机名 处理程序端口 Bifrost IP 或主机名(网络服务) Powercycle IP 或主机名(远程电源管理服务) 首次将设备添加到我们的自动化中时会填充设备映射信息。

当引入新的设备类型进行测试时,设备服务会实现并公开该设备的特定处理程序。设备服务支持以下常用的设备方法集:

请注意,这些端点中的每一个都需要将唯一的设备标识符发布到请求中。此标识符(类似于序列号)与正在操作的设备相关联。

保持服务简单可以使其具有相当大的可扩展性。为设备引入附加功能很容易,如果设备不支持该功能,它只会 NOOPs 它。

设备服务还充当设备池:

以下是我们在实验室中运行的一些自动化设备的图片。请注意 Xbox 360 电源按钮附近的小机械手。这是我们为 Xbox 360 定制的解决方案,因为该设备需要手动按下按钮才能重新启动。我们决定通过设计一个连接到树莓派的机械臂来自动化这个手动过程,该机械臂将控制权交给手来移动和按下电源按钮。此操作已添加到 Xbox 360 设备处理程序。设备服务的 powercycle 端点调用 Xbox 360 的 powercycle 处理程序。此操作对于 PS3 或 PS4 不是必需的,并且未在这些处理程序中实现。

测试服务 测试服务是正在运行的测试用例会话的簿记员。其目的是标记测试用例的开始,记录状态更改、日志消息、元数据、文件链接(在整个测试过程中收集的日志/崩溃小型转储)和测试用例发出的数据系列,直到测试完成。该服务公开了由运行测试用例的测试框架调用的简单端点:

测试框架通常会在内部调用这些端点,如下所示:

测试开始后,调用 POST /test/start 定期的 keepalive 被发送到 POST /test/keepalive 以让测试服务知道测试正在进行中。 在测试运行时使用 POST /test/configuration 和 POST /tests/details 发送测试信息和结果 当测试结束时,调用 POST /test/end

网络服务——Bifröst Bridge

我们为与设备通信并进行流量整形或 dns 操作而构建的网络系统称为 Bifröst 桥。我们不会改变网络拓扑,而是将设备直接连接到主网络。Bifrost 桥不需要运行测试,只有在测试需要网络操作(例如覆盖 DNS 记录)时才可选。

文件服务 当我们运行测试时,我们可以选择收集测试产生的文件并通过文件服务将它们上传到存储库。其中包括设备日志文件、崩溃报告、屏幕截图等……从消费者客户端的角度来看,该服务非常简单:

文件服务由云存储返回,资源被缓存以便使用Varnish Cache快速检索。

数据库 我们选择使用 MongoDB 作为测试服务的首选数据库,因为它的 JSON 格式和它的无模式方面。拥有开放 JSON 文档存储解决方案的灵活性是满足我们需求的关键,因为测试结果和元数据存储始终在不断发展,并且其结构永远不会受到限制。虽然从数据库管理的角度来看,关系数据库听起来很吸引人,但它阻碍了即插即用的原则,因为数据库模式需要手动更新以适应任何可能需要的测试。

在CI模式下运行时,我们为每个测试记录一个唯一的运行 ID,并收集有关构建配置、设备配置、测试详细信息等的信息。文件服务到日志的可下载链接也存储在数据库测试条目中。

测试跑者——迷宫跑者 为了减少每个测试用例所有者调用不同服务和单独运行测试的负担,我们构建了一个控制器来协调运行测试并根据需要调用不同的服务,称为 Maze Runner。

测试套件的所有者创建一个脚本,他/她在其中指定需要运行测试的设备(或设备类型)、测试套件名称和构成测试套件的测试用例,并要求 Maze Runner 执行测试(并行)。

以下是 Maze Runner 执行的步骤列表

根据请求查找要在其上运行的设备 调用设备服务以安装构建 调用设备服务开始测试 等到测试服务中标记为“结束”的测试 显示使用测试服务检索到的测试结果 使用设备服务收集日志文件 如果测试没有开始或没有结束(超时),Maze Runner 使用设备服务检查应用程序是否崩溃。 如果检测到崩溃,它会收集核心转储,生成调用堆栈并通过专有调用堆栈分类器运行它并检测崩溃签名 如果发生崩溃或超时,请通知测试服务。 在序列中的任何时候,如果 Maze Runner 检测到设备存在问题(例如,构建将无法安装或设备无法启动,因为它失去了网络连接),它将释放设备,询问设备服务禁用它一段时间,最终将获得一个全新的设备来运行测试。这个想法是纯粹的设备故障不应该影响测试。 测试框架 测试框架与自动化服务很好地分离,因为它们在设备本身上运行测试。大多数测试可以手动运行,无需自动化服务。这是系统设计的核心原则之一。在这种情况下,测试是手动启动的,并且在测试完成时手动检索和检查结果。

但是,可以使测试框架与自动化服务一起运行(例如,测试服务来存储测试进度和结果)。当我们的运行程序在 CI 中运行测试时,我们需要这种与自动化服务的集成。

为了以灵活的方式实现这一点,我们创建了一个内部称为 TPL(测试可移植性层)的抽象层。测试和测试框架调用这一层,它为每个自动化服务定义简单的接口。每个自动化服务都可以为这些接口提供一个实现。

该层允许我们的自动化运行的测试在完全不同的自动化系统上执行,前提是实现了该系统服务的 TPL 接口。这允许使用其他团队编写的测试用例(使用不同的自动化系统)并以不变的方式运行它们。当测试不变时,完全消除了测试所有者对设备上的测试失败进行故障排除的障碍;我们总是希望保持这种状态。

进步 通过保持测试框架独立于自动化服务,根据需要使用自动化服务并添加缺少的设备功能,我们设法:

扩大我们在游戏机和参考应用程序上的测试自动化覆盖率。 将基础架构扩展到移动设备(Android、iOS 和 Windows Mobile)。 使其他 QA 部门能够利用针对我们的设备基础架构执行他们的测试和自动化框架。 我们最近的测试执行覆盖率数据显示,仅在参考应用程序的每个构建中,我们就执行了大约 1500 次测试。从长远来看,开发团队每天在一个分支上生成大约 10-15 个构建,每个构建为参考应用程序生成 5 种不同的构建风格(例如 Debug、Release、AddressSanitizer 等)。对于游戏机,每天大约有 3 到 4 个构建,具有单一的工件风格。保守地说,使用单一构建工件风格,我们的生态系统负责在给定的一天运行接近 1500×10 + 1500×3 ≈ 20K 的测试用例。

新的挑战 鉴于每天执行的测试数量庞大,出现了两组突出的挑战:

设备和生态系统的可扩展性和弹性 测试结果产生的遥测分析过载 在未来的博客文章中,我们将深入探讨并讨论我们目前为应对这些巨大的新挑战而采取的一系列广泛举措。


原文作者: Netflix Technology Blog 原文链接:Automated testing on devices 发表时间: Aug 10, 2016