本篇是 Airbnb 爱彼迎 Android 自动化测试系列文章的第 2 部分,我们将介绍如何使用截屏测试方法来自动化测试 Fragment 的 UI。
在第 1 篇文章中我们介绍了 MvRx 的状态模拟系统以及能够直接打开 APP 中任一界面或模拟状态的启动器。虽然这个状态模拟系统对于手动测试也很有帮助,但它的最佳适用场景是自动化测试。为了利用好这个测试方式的优势,我们首先实现了自动化截屏测试来检测不同 Commit 之间的 UI 变化。
这有助于发现和解决一系列的产研问题:
间距、颜色和样式的变化
关于数据如何呈现的逻辑问题
如何显示一些边缘情况,像 null 或 empty 数据
从右到左(RTL)布局问题
因为一些通用组件库版本变动带来的变化,如升级了新的 RecyclerView 或 ConstraintLayout 版本
如果这些都使用人工测试的话会花费大量时间,而且我们发现我们在工程师自测和 QA 测试流程中经常无法发现这些问题。尤其是在复杂的界面上,真的很容易漏掉这些问题场景的回归。而自动化截屏测试在可以检测到这些问题之余,还能进行基本的健全性检查,确保 Fragment 可以正常运行而不崩溃。实现自动化截屏测试是我们优化测试流程绝佳的机会点,而且在状态模拟系统就位的情况下很容易就能添加。
我们通过以下几个步骤来实现自动化截屏测试:
构建一个 Android 库来对 Activity 进行截屏并将 Bitmap 上传到云端
使用 Happo[1] 来提供一个 Web UI,展示不同分支之间所截得 Bitmap 的差异
配置持续集成来对每一种模拟状态进行截屏,通过 Happo 生成一份差异报告,再将结果附到 PR 上
这里面的每一步都不容易。
截屏库
想要单单抓取一张截屏十分简单,很多已有的库都能做到;然而,由于有一些重要的需求,我们自己新构建了一个截屏库。
首先,我们需要能够抓取到整个 View 层级,而不仅仅是屏幕上可见的部分。我们的代码中广泛使用到了 RecyclerView。RecyclerView 在布局时不会考虑屏幕之外的内容,因此常规的截屏也就无法截到这些内容。为了解决这个问题,我们手动对 Activity 的 View 进行测量,让它想占用多少高度就占用多少高度。然后,我们再强制进行布局。为了模拟真实的布局过程,我们还调用了已注册的 layout listener 和 pre-draw listener,这些 listener 可能会要求进行再一轮的布局,以此循环直到所有的 View 都布局完成。最后,我们将整个 View 层级绘制到画布上,再保存到一个 Bitmap 中。
另一个需求是尽量减少不同轮次测试间出现视觉差异(即 flakiness)的可能。这个库用下面几种方法最大限度地减少这些差异:
禁用掉 EditText 的光标,不然它会随时间闪烁
清除掉各个 View 的焦点,因为这些焦点的设置在不同的测试轮次可能不一致
对每个 View 进行 invalidate 和 requestLayout,这样可以清除测量过程相关的缓存和 Drawable 的状态,确保每个 View 都会完全重绘
清除掉 Drawable 资源的缓存,因为共享的 Drawable 以不同尺寸重绘时可能会出现不可预测的锯齿
我们的库将每一个 Bitmap 上传到云端,同时将所有的截屏链接地址编译进一份报告,并上传到 Happo。最终的 Happo 报告与一个 git 的 SHA 值对应,Happo 可以将它与任何其他 SHA 值对应的报告来进行对比,以找到不同 git 分支之间的差异。
最后,由于我们需要处理成千上万张截屏,这就要求我们库的性能足够好。为了达到这个目标,我们使用了协程来并发地处理和上传 Bitmap。由于我们是在和 Bitmap 打交道,OutOfMemory 异常会是一个威胁。而我们选择对整个可能无限长的 RecyclerView 进行布局,更是会加剧这个威胁。为了防止这个问题,我们将模拟数据中的所有列表长度截断到了 3 个,但还是支持处理超过一屏幕的内容。通过高效地重用 Bitmap,并启用一个大的堆,我们做到可以对高达 4 万像素长度的 View 进行截屏。我们曾经遇到过到 AWS 和 Happo 的网络请求不稳定的情况,这会引发超时或一些其他不在我们控制中的问题。通过将这些请求封装在一个带有指数退避算法的重试逻辑中,我们显著地提高了测试稳定性。
最终,对于每份模拟数据,我们都有一个对应的图片地址可以一一对应。图片用 Bitmap 的 md5 SHA 值来命名,这样我们可以轻易地判断两个图片是否相同。
Happo
Happo 是一个我们用来做 Bitmap 对比的外部服务。它提供了许多不错的功能,比如:
查看某个界面的 Bitmap 历史记录,了解它是如何随着时间变化而变化
当你关注的界面发生变化时,它能发送邮件警报通知
如果你在图形化界面中将某个差异标记为 flaky,这个标记就会存储起来,这样之后的比较就会忽略这个差异
在差异报告获得批准之前阻断 Github PR
一个可用来查看报告中的所有截屏,并确认不同报告间 UI 变化的 Web 图形化界面
Happo 截屏的差异报告显示每晚价格的展示样式发生了变化
这张图里 Happo 显示了每晚价格展示样式的变化。这个差异报告可以让工程师检查这是否是他们的 PR 预期带来的变化,也能让 review 代码的人更了解这个 PR 是做什么的。所有非预期中的变化都会被轻易发现,并在 PR 合并之前被修复。
这个方法叫做批准测试,它有很多好处:
更新测试用例所需的工作量最小。工程师只需要查看差异报告并批准符合预期的变化。新报告会按标准自动更新
能够完全覆盖 UI 绘制流程,不需要为 UI 手写任何测试代码
轻松地测试 UI 的边缘情况。此系统可以扩展以支持我们需要的尽可能多的模拟数据变式
持续集成配置
为开发者最终提供高度集成化自动化的测试体验,需要将这些部分连接起来,也还有很多重要的工作要做。我们将会在这个系列文章的第 5-7 部分介绍这些细节。
简单来说,我们构建的这个自动化测试框架会自动搜索 APP 中的每一个模拟状态,将其加载到相应界面,然后我们就能在这些界面上运行自动化测试。我们将这个流程做成了通用的,能兼容我们想要进行的任何测试方式 -- 在这种情况下,是截屏测试。
最终的结果是每一个 PR 都会运行一个阻塞的流程来生成截屏并进行对比。如果发现了任何差异,就会在 PR 上增加一条评论,附上发现的差异以及到 Happo 报告的链接。
Github PR 上的一条评论,描述了 Happo 检测到的 UI 变化
这个报告让 PR 的作者和代码 review 的人都清楚地意识到这个 PR 造成了 UI 变化,并精确地指出了变化点。这在检测回归和防止意外代码更改方面都有很大帮助。
除此之外,开发者不需要为配置他们 Fragment 的截屏测试而做任何额外的工作。他们只需要在他们的 Fragment 类中加入模拟状态的定义(如我们在第一部分中所描述的那样),测试框架就会自动使用这些模拟状态来生成截屏。
其他测试
在我们将这个模拟状态测试系统搭建好了之后,做一些其他的检测也变得简单了:
我们在每一个模拟状态截屏时同时配置运行了 LeakCanary 测试。这样如果在测试结束后Fragment、View 或是 Activity 发生了泄漏,我们可以轻易自动检测出来,然后将测试标记为失败
在 Happo 库将整个 Activity 展示出来之后,我们会对其运行 Espresso 无障碍检查,以检测一些常见的无障碍配置违规项
对于 Fragment 的 Arguments 和 State,我们还会模拟进程销毁重建时状态的存储和恢复过程。这个测试用来确定 Arguments 和 State 可以被存储和恢复而不发生崩溃。我们也对重建后的结果进行了截屏,由此来确定 Fragment 是如何恢复被存储的状态的
这个测试框架的奇妙之处在于它为 APP 界面的自动展示和在界面上运行动态生成测试代码奠定了基础。在这个基础上,其他测试都只需很少的代码就能完成设置并立即应用到 APP 的所有 Fragment 上,开发者不需要再做任何额外的工作。由于我们在基础设施的改善增强了可测试性,工程师在创建 Fragment 模拟时的早期投入得到了持续的回报。
下一篇:测试事件的处理
在本篇文章中我们介绍了我们是如何测试界面中的静态 UI 内容的。然而,一个产品功能的大部分代码经常是关于事件处理的,比如界面间的跳转,状态的更新,或是网络请求的执行。
在第 3 部分我们将会沿用 UI 截屏比较的思想,来看看我们如何将这个思想运用到交互测试中,将我们的事件处理代码进行自动化测试。
系列文章概览
本系列文章将系统性介绍爱彼迎 Android 的自动化测试,共包含 7 篇文章。我们将陆续为您推送。
第 1 部分 - 自动化测试的原理与状态模拟系统
第 2 部分 - 利用 MvRx 和 Happo 做自动化截屏测试(本篇文章)
第 3 部分 - 自动化交互测试
第 4 部分 - ViewModel 单元测试的框架
第 5 部分 - 爱彼迎自动化测试框架的架构
第 6 部分 - 如何保持模拟的一致性
第 7 部分 - 测试代码生成及持续集成的配置
引用
https://happo.io/
原文作者:Eli Hart,译者:Xiao Jin,校对:Wankun Yang, Yifan Zhu,Betty Xi。
特别感谢所有项目的参与者,以及相关开源库的贡献者。
感谢阅读!如果你想了解关于爱彼迎技术的更多进展,欢迎关注我们的 Github账号(https://github.com/airbnb/) 以及微信公众号(爱彼迎技术团队)。