Screenshot Test — 通过捕获和比较应用程序屏幕截图与参考图像来自动检查用户界面。与 golden 测试不同,screenshot 测试在真实设备或模拟器上执行,捕获带有导航、系统元素和动画的完整屏幕,并使用 UI Automator (Android) 或 XCUITest (iOS) 与应用程序进行交互。更多信息 — 请参阅Android UI Automator 文档。
核心要点
Screenshot Test — 是用户界面的端到端测试,测试打开应用程序屏幕,执行操作(点击、输入文本、滚动)并捕获所得状态的截图。截图与存储在仓库中的参考图(baseline)进行比较。如果截图不同 — 测试失败。Screenshot 测试发现单元测试中看不到的视觉回归:错误的边距、元素重叠、错误的颜色。
为什么有了 golden 测试还需要 screenshot 测试 — golden 测试单独检查组件:一个按钮、一张卡片、一段文本。Screenshot 测试在尽可能接近生产环境的情况下检查整个屏幕:真实导航、真实数据(或尽可能真实的 mock)、真实系统字体、真实状态栏。只有 screenshot 测试能显示按钮在真实设备上与其他元素重叠。
商业价值 — 根据 Google(2023)的数据,视觉错误占移动应用所有错误的 15-25%。Screenshot 测试自动化了以前由 QA 工程师手动执行的视觉质量检查。一个 screenshot 测试可替代 5-10 分钟的单屏幕手动测试。对于拥有 50 个屏幕的应用,节省:每次回归测试节省 4-8 人时。Screenshot 测试在 2-3 个发版周期内即可收回成本。
Golden 测试 更快更简单:在离屏缓冲区中渲染组件只需毫秒,不需要设备,在 CI 中稳定。Screenshot 测试更真实:捕获带有系统元素的真实屏幕,支持动画和导航,在真实设备上工作。选择取决于目标:为开发人员提供快速反馈(golden)或在发版前实现最大真实性(screenshot)。
| 特性 | Screenshot Test | Golden Test |
|---|---|---|
| 速度 | 2-30 秒 | 50-200 毫秒 |
| 真实性 | 最大(真实设备) | 有限(离屏) |
| 需要设备 | 是(模拟器/物理设备) | 否(JVM、XCTest) |
| 动画 | 支持 | 不支持 |
| 导航 | 多步骤场景 | 单个组件 |
| Flakiness | 高(网络、时序) | 中(GPU、字体) |
| 并行性 | Device Farm (Firebase、AWS) | 多线程 JVM/XCTest |
Golden + Screenshot — 对组件库(Design System)中的每个 UI 组件使用 golden 测试。80% 的视觉回归在组件层级被捕获。Screenshot 测试 — 用于关键用户路径:入门引导、登录、支付流程、购物车。20% 与组件在真实屏幕上集成相关的回归只能被 screenshot 测试捕获。在 IT Sectr,我们使用 80/20 的比例:400 golden + 100 screenshot。
什么时候不需要 screenshot 测试 — 如果屏幕由静态内容组成且没有交互性,组件的 golden 测试以更低的成本提供同等级别的检查。如果屏幕动态变化(流、聊天),screenshot 测试需要复杂的数据配置和等待时间。在这种情况下,对基础状态(空列表、加载中)使用 screenshot,对列表中的单个卡片使用 golden。
UI Automator — 用于跨应用程序 UI 测试的 Android 框架。可以通过 UiDevice.takeScreenshot() 捕获截图。与 Espresso(在单个应用程序内部工作)不同,UI Automator 可以与系统对话框(权限、通知)和其他应用程序进行交互。UI Automator 上的 Screenshot 测试:打开应用程序,等待加载,捕获截图,与参考进行比较。
class LoginScreenScreenshotTest {
@get:Rule
val rule = ComposeTestRule.createAndroidComposeRule<MainActivity>()
@Test
fun login_screen_default() {
val device = UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation()
)
// 等待屏幕加载
IdlingRegistry.getInstance().waitForIdle()
// 进行截图
val screenshot = device.takeScreenshot()
val golden = loadGolden("login_default.png")
// 与基准比较
val diff = ImageComparator.compare(screenshot, golden)
assertTrue(diff.similarity > 0.98)
}
}
Firebase Test Lab — Google Cloud 服务,用于在数百台真实设备上并行运行工具测试。Firebase Test Lab 上的 Screenshot 测试在不同设备(Pixel 7、Galaxy S24、Xiaomi 14)上捕获截图并与参考图进行比较。优势:一个测试在 10-15 分钟内检查 20 台设备上的 UI。缺点:成本(每次测试在 20 台设备上 $1-5)。Firebase Test Lab 通过 gcloud CLI 或 Gradle 插件与 CI 集成。
Shot — Android 上用于 screenshot 测试的库,方便截图的创建和比较。Shot 基于 Espresso 和 UI Automator 工作,添加了 golden 管理(创建、更新、删除)、阈值比较(像素或百分比)和 HTML 报告生成。Shot 适合希望快速实施 screenshot 测试而无需编写自己的图像比较基础设施的项目。
XCUITest — Apple 框架,用于 iOS、iPadOS 和 tvOS 应用程序的 UI 测试。XCUITest 上的 screenshot 测试使用 XCUIScreen.main.screenshot() 捕获屏幕,并使用 XCAttachment 保存截图。XCUITest 模拟用户操作:tap、swipe、typeText,并在每个步骤后捕获截图。在 Xcode 16+ 中,添加了通过 XCTAttachment 将截图与参考图进行比较的内置支持。
final class LoginScreenScreenshotTests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
app = XCUIApplication()
app.launch()
}
func test_login_initial_state() {
let loginButton = app.buttons["login_button"]
XCTAssertTrue(loginButton.exists)
// 进行截图
let screenshot = app.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "Login-Screen-Initial"
attachment.lifetime = .keepAlways
add(attachment)
// 与基准比较(需要 XCTAttachment + golden)
assertScreenshot(
screenshot: screenshot,
goldenName: "login_initial_state"
)
}
}
Xcode Cloud — Apple 的云 CI,用于构建和测试 iOS 应用程序。Xcode Cloud 支持在模拟器上运行 XCUITest 测试。Screenshot 测试可以在多个模拟器上并行运行(iPhone 15、iPhone 15 Pro Max、iPad Pro)。结果:带附件的 XCResult Bundle。Xcode Cloud 未内置在 GitHub/GitLab 中 — 请使用 Xcode Cloud Webhooks 进行集成。替代方案:使用 macos-14 和 xcodebuild 的 GitHub Actions。
比较框架 — iOSSnapshotTestCase (Uber) 如果在模拟器上运行,也可用于 screenshot 测试。SwiftSnapshotTesting (pointfree) 更专注于组件的 golden 测试。对于 iOS 上的 screenshot 测试,请使用内置工具 XCUITest + XCTAttachment + 自定义 ImageComparator(Pixelmator 或 AImage)。在 CI 中使用模拟器 — 在真实设备上,screenshot 测试只能通过 Device Farm(AWS Device Farm)工作。
Baseline 管理 — 参考截图存储在仓库(Git LFS)或 S3 中。每个截图按照模板命名:{testName}_{device}_{orientation}_{locale}.png。示例:loginScreenPixel7PortraitRu.png。添加新设备或 locale 时,会创建新的 baseline。UI 变更时,旧的 baseline 在 code review 后被新的 baseline 替代。Baseline 是代码库的一部分,就像测试源代码一样。
CI Pipeline — (1) 构建应用程序。(2) 在模拟器/仿真器上运行 screenshot 测试。(3) 将截图与 baseline 进行比较。(4) 如果不匹配 — 生成 diff 图像。(5) 上传 diff 特征物(actual、expected、diff — 三个文件)。(6) 发布带有结果表的 HTML 报告。(7) 如果超过阈值 — 测试失败。(8) 审查人员检查 diff 特征物并做出决定:批准(更新 baseline)或拒绝(修复代码)。
阈值和容限 — 绝对的像素级比较过于严格。请使用 SSIM(Structural Similarity Index)或 MSE(Mean Squared Error)。SSIM 0.98 = 98% 结构相似度 — 一个好的阈值。不同屏幕可能需要不同的阈值:深色主题(更多黑色 — 更高精度)、渐变(更多噪声 — 更低精度)。通过参数每个测试单独配置阈值:@ScreenshotTest(threshold = 0.99)。
Device Farm vs 模拟器 — 在真实设备(Firebase Test Lab、AWS Device Farm)上的测试提供最大真实性,但缓慢且需要付费。在模拟器/仿真器上的测试 — 快速免费,但无法展示真实设备特征(不同 GPU、屏幕显色、像素密度)。策略:模拟器用于 pre-merge 检查(5 分钟),Device Farm 用于 nightly(30 分钟,20 台设备)。在 IT Sectr,我们使用 Firebase Test Lab 在顶级 10 款 Android 设备上进行夜间运行。
常见问题
Golden Test — 用于在每次提交时快速检查单个 UI 组件(50-200 毫秒)。Screenshot Test — 用于在发版前在真实设备上对完整屏幕进行 E2E 检查(2-30 秒)。两者都用:golden 用于 Design System 组件,screenshot 用于关键用户路径。80/20 的比例对大多数项目来说是最佳的。
SSIM 0.98 — 对大多数屏幕来说是一个好的起始阈值。对于深色主题可以使用 0.99(更高对比度 — 更精确的比较)。对于带有渐变和图像的屏幕 — 0.95-0.97。不要使用绝对像素级比较(MSE = 0) — 由于反锯餍和 GPU 差异,它会产生 20-30% 的假阳性。针对每个测试单独配置阈值。
每次故意的 UI 变更时 — 颜色、字体、边距、图标的变更,添加/删除元素。不要在环境变更时更新 baseline(操作系统版本、CI 中的字体) — 这是 flaky 测试的标志。Baseline 只由开发人员在 code review 后本地更新:删除旧的 baseline,以 record=true 运行测试,检查新的截图,提交代码。
可以 — 通过 Android 上的 Espresso 和 iOS 上的 XCUITest。Espresso 在应用程序进程内部工作,不需要 Accessibility Service(像 UI Automator 那样)。XCUITest — Apple 的标准 UI 测试框架。对于 screenshot 测试,差异很小:XCUITest 稍微更稳定(原生 Apple API),UI Automator 稍微更灵活(进程间通信)。
如果配置得当 — 不会。Pre-merge:只在变更的屏幕上运行 screenshot 测试(30-60 秒)。Nightly:在 Device Farm 上完整运行(30 分钟,20 台设备)。在模拟器上 screenshot 测试的执行时间:每个屏幕 2-10 秒。20 个屏幕 = 40-200 秒。这比手动测试一个屏幕的时间(5-10 分钟)还要少。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。