背景:高失败率对CI流程的阻碍

Shopify的核心DNA中,QA文化占据重要地位。对于其移动应用而言,这种文化依赖于测试金字塔顶端的一层端到端(E2E)测试。这些测试模拟商家真实操作,并在每次拉取请求(PR)时作为阻塞性CI运行。只要测试套件值得信赖,这种金字塔结构就能发挥巨大作用。

然而,Shopify最大的移动应用曾陷入困境:测试阻碍了好于坏的开发进度。由于屏幕加载时间波动导致测试变得不稳定(Flaky),最终不得不将E2E套件从PR检查中完全移除。这一问题的根源在于旧有架构的局限性,而非测试人员的能力不足。

"The Shopify mobile app (our largest) had gotten to the point where it was blocking more good PRs than bad ones."

旧架构的缺陷:Appium与Test ID的双刃剑

自2023年起,Shopify使用基于WebdriverIO的Appium运行E2E测试,并通过React Native Test ID定位元素。这种灵活性最终变成了负担:Appium提供了底层控制力,但未强制良好的测试模式。开发者在点击一个元素后,往往立即尝试点击下一个元素,而此时新屏幕尚未渲染完成,导致“未找到元素”的失败。

虽然理论上应显式等待元素出现,但开发者容易使用pause(1000)这种硬编码延迟。这在本地通常有效,但在CI环境中一旦屏幕加载稍慢便会失败。这些捷径堆积起来导致了测试的不稳定性。此外,旧API经常断言错误的对象:它仅检查组件树中是否存在节点,而非商家实际可见或可交互的内容。测试实现细节而非用户体验是核心误区。

"The problem wasn’t that we were bad at bailing out the suite; it was that the framework itself kept creating the same failures."

重构方案:构建 opiniated Wrapper

Shopify决定不再修补Appium,而是围绕其构建一个强制性的封装层(Wrapper)。该方案包含两部分:一是严格的Builder风格API,使编写不稳定测试变得困难;二是计算机视觉技术,以用户视角而非遍历视图层级来定位元素。底层仍由Appium驱动设备,但开发者无法绕过封装层执行危险操作。

这种设计确保了测试框架本身不再制造相同的失败模式。开发者被限制在安全护栏内,无法再使用导致崩溃的底层API调用。这一转变标志着从“管理不稳定性”到“消除不稳定性根源”的策略升级。

新API设计:Builder模式与强制断言

新的测试API采用Builder风格,仅暴露经过验证不会导致不稳定的动作。其核心设计原则包括:

  1. 每一步都携带断言:点击、等待或输入后,必须声明屏幕应显示的状态。如果应用状态偏离预期,测试会在现实与期望分叉的步骤失败,而不是在后续动作中崩溃。
  2. 可复用切片:如logIntoApp这样的命名步骤序列,任何测试均可调用。
  3. 安全出口标记:绕过护栏的选项(如自定义超时或脚本注入)以UNSAFE_前缀命名,以此警示开发者并促进代码审查。
  4. AI友好性:语法可预测且表面区域小,使得人类和AI工具都能更准确地生成测试代码。

"Every step carries an assertion. You can’t tap, wait, or type without declaring what the screen should show afterward."

视觉定位取代Test ID:计算机视觉的应用

底层技术的重大变革在于使用截图和视觉匹配来定位目标元素。系统通过PaddleOCR处理文本,利用OpenCV将截图与Polaris设计系统的SVG进行匹配。Test ID仅作为视觉匹配不可靠时的备选方案,并通过UNSAFE_testID字段鼓励谨慎使用。

这一改变显著提升了编写速度。开发者无需深入组件树查找或添加Test ID,只需观察模拟器屏幕上的文本(如“Save”)并编写touch({ text: 'Save' })即可。对于AI代理而言,这种语法与屏幕内容的一一对应关系意味着无需了解代码库即可生成正确测试。

"With computer vision, you look at the simulator, see 'Save', and write touch({ text: 'Save' }). And that’s the whole loop."

可观测性与统一CLI体验

每次运行都会生成标注视频,展示每一步搜索的目标文本、OCR识别结果及点击位置。当测试失败时,开发者可通过几秒的视频快速诊断原因(如OCR搜索的文本与实际发现的差异),无需重新运行测试。

此外,Shopify开发了一个统一的CLI工具。通过单一命令即可在本地模拟器、CI环境或远程设备农场中运行测试。例如,使用--runner remote-device-farm调用云端真机,或切换为--runner local在本地运行。命令的一致性消除了环境差异带来的配置负担。

"Most failures diagnose themselves in a few seconds of video, with no rerun needed."

迁移成果:稳定性数据与门禁机制

在将新API推广至Shopify应用的阻塞性CI后,测试稳定性从旧API的50%提升至98%。这一数据定义为单个测试成功次数除以总运行次数。剩余的失败主要源于预期的外部因素,如网络波动或模拟器启动异常。

为确保质量持续可控,Shopify建立了预发布不稳定性门禁(Flakiness Gate)。新测试在加入阻塞套件前,必须在专用管道中多次运行;若失败率超过设定阈值则被拒绝。这一机制从源头遏制了不稳定测试的引入。

"98% test stability, measured as individual test successes divided by total runs—up from 50% using the old API."

实施指南:构建可靠测试框架的关键原则

若其他团队面临类似困境,Shopify分享了以下核心实践:

  • 限制API命令集:仅保留DeepLink、Swipe、Type、Touch、Assert和Relaunch App等基础命令。
  • 采用计算机视觉:使用PaddleOCR处理文本,OpenCV通过灰度转换和颜色反转匹配图标。对于重复元素,利用相邻关系(如“icon1在icon2左侧”)进行消歧。
  • 强制动作断言:每个操作必须包含验证,且需确保断言在动作前为假、后为真。
  • 合并前验证稳定性:测试必须在多次运行中证明其稳定性,方可合并。

"When we replaced the API... we found that a suite that couldn’t stay in blocking CI runs at 98% stability on two platforms."

结论

Shopify的经验证明,移动端E2E测试的不稳定性主要源于API设计缺陷而非测试本身。通过构建强制断言、采用计算机视觉定位以及限制危险操作的封装层,团队成功将测试稳定性从50%提升至98%,使其能够稳定运行于阻塞性CI中。这一转变不仅恢复了测试套件的可信度,还通过简化语法提升了AI辅助开发的能力。在工程速度日益重要的今天,可靠的测试框架是保障快速迭代而不牺牲质量的关键基础设施。