对抗脆弱性
Contents
[NOTE] Updated October 27, 2022. This article may have outdated content or subject matter.
背景
4 月的时候上了一个新项目, 项目是搭建一个平台给 flutter 应用做 E2E 自动化测试,项目架构可以简化成下图.
- 最底层绿色的部分是 flutter 官方 SDK, 其中主要使用了 integration_test 的包, 其中深绿色的小块是我们做的扩展. 项目初期的同事们做了许多调研性的工作, 搭建了平台, 对 flutter 自动化测试能力摸底, 把很多可能的坑都暴露出来, 包括 flutter 本身技术限制或 bug 导致测试能力局限. 并做了 workaround 的填补, 对测试能力进行扩充。比如支持文件上传和下载文件检查,支持邮件内容检查.
- 第二层黄色的部分是要测的应用程序.
- 第三层红色的部分是我们的测试平台, 使用了一个叫 flutter_gherkin 的包, 可以把最上层的 cucumber test 转化为可执行的测试代码, 测试代码和项目的源代码一起打包运行在浏览器中, 最后由 chromedriver 生成出每一步的测试结果报告.
- 最上层白色的部分就是 测试人员利用我们在第三层写的 step 来写一个个具体的测试用例.
理想中通过复用第三层的 step 库, 可以对任意 flutter 应用进行自动化测试, 测试人员需要做的只是在上层写新的测试用例.
概念验证完, 我们开始对测试平台进行工程化, 大量书写 E2E 测试并使之能稳定运行在上面, 从而实现对产品代码 nightly watch. 没想到我们的 nightly 噩梦开始了.
挑战
理论上按照官方 tutorial, 引入 integration_test 包和 flutter_gherkin 包并配置好, step 库写完, hello world 的测试一跑, 一切就完美结束了. 但实际上面对真实的工程, 我们遇到了极大的挑战.
作为测试金字塔最顶端的 E2E 测试本身就有测试用例维护成本高, 测试不稳定的问题, 因为它集成了所有的模块, 任何一点波动 (网络,代码更改。。) 都可能破坏它,就好像上游任何人排脏水都会污染到你这里来.
我们的自动化测试平台也不例外,除了其它上面提到的问题之外, 它还有以下的挑战。
- 根基的不稳定。 我们至少发现并提出了 6 个 flutter integration_test 的关键问题,比如每个测试运行起来之后, 都会实际被运行 2 次, 这对于一些进行检查写操作的测试来说是灾难性的, 但不幸这些问题至今仍被埋在了 flutter 5K+ 的 issue 里. 最近客户通过和 google 的商业伙伴关系,帮我们找到了 flutter 团队的 PM 和 Dev, 有望一起推进问题的解决.
- flutter cucumber 测试 是个小众领域, 我们使用的 flutter_gherkin 包也是一个缺乏维护的包, 尤其是对 flutter web 的支持更少, 整个团队的小伙伴都没有太深 dart 和 flutter 能力, 我们只能根据需要自己维护它.
- 我们要测的应用是一个很大的应用, 至少 6 个团队工作在上面, 平均每天有 30+ PR 被 merge 回来, 在应用代码快速迭代的过程中, 我们的自动测试平台也经历不断被摧毁的麻烦, 诡谲的是每次自动化测试平台坏了的时候, 手动测试却无法复现问题, 人家的代码运行的好好的. 一部分原因在于我们想做的是 E2E 测试, 利用的实际却是一个集成测试的包(integration_test), 测试并非运行在一个部署好的应用上, 而是用源代码启动起来跑一次, 源码中一些隐藏的 error 都会让测试莫名 crash.
- 客户的测试人员也对我们提出很高的要求, 比如他们期望我们不要用 key 来准确定位 widget, 而是用页面上可读的元素来定位要操作的 widget. 这样将来他们会更容易写测试, 但实际中页面上相近的元素在代码的 widget tree 上可能却是平行的关系或者完全没有关系, 这为 我们构建测试步骤添加了很多麻烦, 也加深了我们的测试平台和要测试的应用之间的耦合.
- 测试集成非常困难, 单独一个测试单独跑很正常, 但放在一起跑就会非常脆弱, 会随机出现问题. 放在一起的测试数量越多就会越脆弱, 出现的问题就会越离谱.
总的来讲所有挑战中最大的就是它脆弱性, 我们利用的工具不稳定, 要测的应用也不稳定, 我们没有能力修复底层的不稳定. 改不了 flutter 的 issue, 也改不了应用的问题, 那有没有可能基于这些不可靠的底座做为工作起点来实现可靠的上层呢? 让用例可以稳定跑起来, 能够找出可复现的 bug, 为 QA 和开发带来价值.
历史上有很多这样的先例. 操作系统在不可靠的物理元件基础上实现可靠的计算, TCP 在不可靠的网络中实现可靠的信息传输. 这些例子中很多思想给我们打开了很多思路, 具体我们使用了下面的方法:
方法
保持简单
保持测试代码简单
我们的代码大部分都是用来测试的, 它本身很难再写代码去测试, 保证它可靠最简单的办法就是让我们的代码保持简单和直观, 比如找到按钮, 点击,或者找到输入框, 输入文字. 不去包含多余的逻辑, 失败就让它简洁明了的失败.
保持架构简单
测试是运行在 browser 的 sandbox 中, 受限于浏览器的能力, 测试无法读写电脑中的文件, 也无法访问邮箱读取邮件, 起初的扩展方法是在 flutter sdk 里修改代码来支持这些能力, 但这也带来了很多的问题, 1. 在最底层做修改, 能力有了, 但这些代码质量会对上层的稳定带来更大的隐患, 2. 我们的 flutter 无法升级, 我们需要不断维护这些底层的修改. 当时改底层代码的同事早已离职, 我们也失去了维护这些 hack 代码的能力. 于是 7 月在升级 flutter3.0 之际, 我们想了一个更简单的办法: 调整架构.
我们把之前对底层的修改全部去除, 既然是因为 sandbox 限制导致的, 我们直接加个后端就好了, 后端 api 来 check 文件, 邮件, 甚至未来任何的检查和操作, 把 check 结果通过 http 告诉测试就可以了, step 从而判断当前是否和预期一致. 调整后的架构更简洁, 而且后端独立在测试平台之外, 不会对测试的稳定性带来任何影响.
适度冗余和出错重试
由于网络等原因, 页面加载时间不稳定, 我们在测试中添加冗余和等待,比如等页面加载停止之后再操作,或者先 check 元素出现再操作。这种方法确实增加了稳定性,但也增加了复杂度,代码和用例中到处散落的等待 3 秒, 5 秒也会让后来的维护者一头雾水。而且不稳定是随机的,太多不必要的 pause 也托慢了测试的运行速度。
后来我们想到了更好的办法:出错重试。通过添加 几行代码系统性地解决了这个问题: 每个 step 在失败的时候都再会重试几次,比如点击 button 之后 check 文字,如马上 check 可能会失败, 如果失败, 我们就等 3s 再重试, 直到 5 次之后都失败, 我们才认为他真正失败了.
|
|
隔离变化
我们首先分析了哪些变量的变化会对测试结果产生影响. 下图中可以看到有 3 个主要的变量: 开发团队的代码, 我们的测试代码, 还有不稳定的测试数据.
为了快速找到测试失败的原因, 我们只能一次允许一个变量存在. 比如过去几天测试一直没问题,今天更新了开发的代码, 测试挂了, 重试也不行, 我们就可以认为开发的代码导致了测试失败.
- 首先隔离测试数据的变化 测试的应用提供了一个可以快速创建测试帐号, 并自动销毁的 api, 我们利用这个 api 为每个测试在运行前都创建一个全新的帐号, 并利用其它后端 api 为这个帐号准备相应的数据. 通过这个方法彻底保证了测试数据的稳定.
- 隔离开发代码和测试代码. 开发代码和测试代码都在快速迭代之中, E2E 不像 UT 可以快速执行, 一般每天晚上回归一次, 某一天测试挂了, 那到底是谁的问题呢? 要隔离两边的代码最简单的方法就是分开建 2 个 pipeline.
如图可见, 从我们的测试代码中切出一个稳定的 release 分支, daily 的 CI 上每天运行最新的开发代码和我们稳定的 release 代码, 用来检测开发代码是否正常, prepration 的 CI 上运行一个稳定的开发代码和我们日常工作的测试代码. 每个 CI 上都只有一个变量, 当发生异常, 我们就可以快速知道问题是哪来的.
隔离环境
为了让所有的测试用例每晚回归一次, 我们把它们放在一个 test plan 里, 并集成在一起触发.但一个脆弱的基座上堆越多积木, 就越容易倒, 用例也一样, 集成的越多就越不稳定. 随着我们的测试数量越来越多, 常常 nightly watch 只跑了几个 case 就出现异常并退出, 导致第二天需要花人力把没跑的 case 找出来, 在 CI 上再单独触发.
如果每次只跑一个 case 不就稳了么? 我们试图在 case 运行之间重启应用或者重置应用状态, 但 flutter 不支持. 最后我们利用 gherkin 的 tagExpression, 写 shell 脚本帮我们自动装载新用例, 最终让 case 一个一个地跑.
后续
这些方法最终结束了我们的噩梦, 在过去的几周里包括国庆长假, 整个测试系统在无人看管的情况下稳稳地运行着, 每天跑完所有测试并生成测试报告, 经验证报告出的 issue 可复现, 让测试平台达到了可用, 每天稳定地产出真实的 bug.
Author Lin Chen
LastMod 2022-10-27 (d41e38d)