我们如何将移动端端到端测试稳定性提升至 98%
原文链接:How we raised mobile end-to-end test stability to 98%
发布于 2026 年 8 月 12 日
背景:测试套件成为开发瓶颈
Shopify 移动应用(Shopify 最大的应用)的 E2E 测试套件长期运行在 Appium + WebdriverIO 之上,通过 React Native Test ID 查找元素。这套架构在 2023 年引入时提供了充分的灵活性,但也埋下了不稳定的根源。
问题出在等待策略上:点击元素后,测试可能在新屏幕渲染完成前就尝试下一步操作,导致"找不到元素"失败。开发者最常见的补救方式是随手加 pause(1000)——这在本地运行看似正常,在 CI 上大部分时间也能通过,直到某个页面加载稍慢,测试就挂了。
测试套件拦截的合格 PR 比不合格的还多,最终被迫从 PR 检查中完全移除。
旧架构的缺陷
旧测试框架存在三个核心问题:
没有强制等待机制。 显式等待(waitForDisplayed)是正确的做法,但框架允许使用固定的 pause() 作为捷径,久而久之养成了脆弱的测试习惯:
// 正确的做法:
const addProductButton = $('~addProductButton');
await addProductButton.waitForDisplayed();
await addProductButton.click();
// 诱人的捷径:"就等一秒让屏幕出来。"
await driver.pause(1000);
await $('~addProductButton').click();
断言的是实现细节而非用户体验。 测试验证的是组件树中存在某个节点,而非商家真正能看到或使用它。例如,底部插图中被遮挡的单元格,旧 API 可以点击到并错误地通过测试。
重复修补无法解决根本问题。 问题不在于团队不擅长维护测试,而是框架本身持续制造同样的失败模式。
重构方案:有主见的包装层 + 计算机视觉
团队没有替换 Appium,而是围绕它构建了一个严格、有主见的包装层。底层仍由 Appium 驱动设备,但开发者无法绕过包装层去使用那些曾导致不稳定的功能。方案包含两部分:构建器风格的测试 API 和计算机视觉元素查找。
构建器风格的测试 API
新 API 只暴露经过验证、不会产生不稳定行为的操作:
createAutomationTest({
title: 'Product Creation',
description: 'Creates a product and saves it',
})
.includeSlice(logIntoApp)
.sendDeepLink({
path: '/products',
assert: {
text: 'Filter products',
description: 'Product list screen was displayed',
},
})
.touch({
icon: 'PlusCircleIcon',
assert: {
text: 'Select category',
description: 'Create product screen was displayed',
},
})
.type({
text: 'My Awesome Product',
assert: {
text: 'My Awesome Product',
description: 'Entire product name was inputted successfully',
},
});
四个关键设计决策:
- 每一步都携带断言。 点击、等待或输入必须声明操作后屏幕应显示的内容。如果应用偏离预期状态,测试会在分歧发生的那一步失败,而不是在几步之后下游出错时才暴露。
- 可复用的切片(Slices)。
logIntoApp是命名的步骤序列,任何测试都可引用。 - 逃生舱以
UNSAFE_为前缀。 确实存在绕过护栏的选项(如自定义超时或脚本注入),但命名刻意劝阻使用。测试中出现UNSAFE_timeoutInSeconds即预示需要人工审查。 - 对 AI 代理友好。 API 表面积小、语法可预测,人类和 AI 工具都能更频繁地一次写出正确的测试。
计算机视觉替代 Test ID
更大的改变在元素查找层。每一步截取屏幕截图,以视觉方式找到目标——就像商家看到"保存"按钮然后点击它:
- 文本识别:使用 PaddleOCR 处理屏幕上的文本。
- 图标匹配:使用 OpenCV 将截图与 Polaris 设计系统中的 SVG 图标匹配。所有图像先转换为灰度,然后在多种尺寸变体中匹配图标(含反色版本),直到找到匹配项。重复元素通过邻近关系(如"icon1 在 icon2 左侧")消除歧义。
- Test ID 退居后备:对于生成内容导致视觉匹配不可靠的屏幕,Test ID 仍可使用,但必须通过
UNSAFE_testID字段显式选择。
写测试的速度显著提升。用 Test ID 时,添加一步意味着打开检查器、深入组件树寻找或添加 testID、再接线。用计算机视觉时,看看模拟器看到"保存",写下 touch({ text: 'Save' }) 就完成了。AI 代理同样受益:语法与屏幕内容一一对应,"编写一个创建产品的测试"这条指令无需代码库知识就能在第一次尝试时生成正确代码。
每次运行生成带注释的视频,显示每一步在寻找什么、在何处查找。测试失败时,能直接看到 OCR 在搜索哪个文本、实际找到了什么、点击了哪里。大多数失败仅需观看几秒视频即可自我诊断,无需重新运行。
单一 CLI 跨环境运行
运行器是单一命令,在笔记本、CI 模拟器或远程设备农场的真机上工作方式完全相同:
mobile-e2e test \
--runner remote-device-farm \
--config mobile-e2e.remote-device-farm.json \
-p ios \
logout
将 --runner remote-device-farm 替换为 --runner local,同样的命令在本机模拟器上运行。
迁移结果:稳定性从 50% 提升至 98%
新 API 在 Shopify 应用纳入 CI 阻塞检查的几周后:
- 测试稳定性达到 98%(单次测试成功次数 ÷ 总运行次数),旧 API 的稳定性仅为 50%。
- 剩余失败大多在预期内:偶发网络故障和模拟器启动失败。
推广前稳定性门槛:新测试被允许进入阻塞套件前,专门的流水线会多次运行该测试,失败率超过设定阈值则拒绝纳入。
对开发者的启示
Shopify 团队总结的四个可复用的原则:
- 将 API 限制为一小组基本命令。 深度链接(Deeplink)、滑动、输入、触摸、断言和重启应用。
- 使用计算机视觉与屏幕上的文本和图标交互。 团队评估了多个开源 OCR 库,PaddleOCR 是明显的赢家。
- 要求每个操作包含断言或反证。 防止测试在未验证任何实际发生的情况下继续推进。此外,断言必须在操作前为假、操作后为真。
- 在合并前确保测试稳定性。 测试只有在多次运行中证明自身稳定后才被允许进入阻塞套件。
结论
移动端 E2E 测试的不稳定性并非天生不可避免。当用新原则(每步断言、像用户一样查找元素、拒绝隐患)替换 API 后,一个无法留在 CI 阻塞检查中的测试套件,现在以 98% 的稳定性在两个平台上运行。随着 AI 提升工程速度,这样的框架让团队能够在不失去发布信心的前提下加速前进。
播客全文
Shopify 工程团队对移动端端到端(E2E)测试框架进行了重构,将测试稳定性从 50% 提升至 98%。旧系统基于 WebdriverIO 和 Appium,依赖 React Native Test ID 查找元素,缺乏强制性的良好测试模式,导致开发者通过显式等待或 pause 命令应付页面加载延迟,测试经常拦截合格 PR。根本问题在于框架本身持续制造不稳定,而非开发者能力不足。
重构方案包含两部分。一是严格、构建器风格的 API,每一步操作必须携带断言,测试在预期与现实出现分歧的步骤立即失败,并引入可复用切片和以 UNSAFE_ 前缀标记的逃生舱。二是用计算机视觉替代 Test ID,通过 PaddleOCR 处理文本、OpenCV 匹配 Polaris 设计系统中的 SVG 图标,测试以用户视角查找元素,编写速度显著提升,AI 代理也能一次生成正确代码。每次运行生成带注释视频,失败时可直接定位原因。
迁移至 CI 阻塞检查后,测试稳定性达到 98%,剩余失败多为偶发网络故障和模拟器启动问题。新测试在纳入阻塞套件前需在专用流水线多次运行,失败率超过阈值则被拒绝。团队正在探索将该框架推广至其他应用,并建议其他团队将 API 限制为基本命令集、使用计算机视觉交互、要求每步断言并在合并前验证测试稳定性。
参考链接
迅步科技专注Shopify独立站,Shopify应用及无头网站开发,为您的品牌提供快速、灵活、个性化的电商体验,提升用户转化率与运营效率。欢迎咨询合作。
