同一段 iOS 界面代码在开发机上看起来正常,合并后却可能出现按钮被截断、动态字体挤压、深色模式颜色失配等问题。视觉回归门禁的目标不是替代单元测试,而是在固定渲染条件下回答一个具体问题:这次提交是否改变了用户真正看到的像素。
云端 Mac 适合持续执行这类任务,但前提是把模拟器状态、测试数据和比较规则都纳入版本控制。只保存几张“看起来正确”的截图,无法形成稳定门禁。
先定义稳定的截图矩阵
不要从“覆盖所有页面”开始。先挑选登录前首页、核心列表、详情页、空状态和错误状态等高价值界面,再为每个界面固定以下维度:
| 维度 | 建议固定值 | 变更时的处理 |
|---|---|---|
| 设备 | 一个明确的 Simulator 型号 | 新建独立基准目录 |
| 系统 | 指定可用运行时 | 重新评审基准图 |
| 外观 | 浅色、深色分别执行 | 不跨外观比较 |
| 语言 | 每种目标语言独立截图 | 文件名带语言代码 |
| 字号 | 默认字号起步 | 大字号另建测试组 |
| 数据 | 本地夹具或测试接口 | 禁止依赖随机内容 |
基准路径可以采用 Snapshots/<runtime>/<device>/<locale>/<appearance>/。目录层级虽然稍长,但失败时能直接看出比较发生在哪个环境,避免把不同系统的渲染差异误判为产品回归。
基准图不是永久正确答案。它是经过评审的界面契约,任何更新都应与代码变更一起接受审查。
固定模拟器而不是反复复用现场
长期复用一个手工操作过的模拟器,会残留权限弹窗、键盘设置、缓存和测试账户。更可靠的方式是给视觉任务分配专用设备,在每轮运行前关机、清除并重新启动。
#!/usr/bin/env bash
set -euo pipefail
: "${DEVICE_UDID:?DEVICE_UDID is required}"
xcrun simctl shutdown "$DEVICE_UDID" 2>/dev/null || true
xcrun simctl erase "$DEVICE_UDID"
xcrun simctl boot "$DEVICE_UDID"
xcrun simctl bootstatus "$DEVICE_UDID" -b
xcrun simctl status_bar "$DEVICE_UDID" override \
--time 09:41 \
--batteryState charged \
--batteryLevel 100 \
--wifiBars 3 \
--cellularBars 4
rm -rf TestResults/Visual.xcresult
xcodebuild test \
-workspace Example.xcworkspace \
-scheme ExampleVisualTests \
-destination "id=$DEVICE_UDID" \
-resultBundlePath TestResults/Visual.xcresult
rm -rf TestResults/Attachments
xcrun xcresulttool export attachments \
--path TestResults/Visual.xcresult \
--output-path TestResults/Attachments
不要依赖当前启动的 booted 设备。并行任务可能同时启动多个模拟器,明确传入 UDID 才能保证状态栏覆盖、安装和测试落到同一台设备。
用可观察状态触发截图
视觉测试最常见的错误是启动应用后固定等待两秒。机器负载或网络变化会让两秒时而过长、时而不足。XCUITest 应等待一个代表“页面已经可比较”的可访问元素。
func capture(_ name: String, readyIdentifier: String) {
let app = XCUIApplication()
app.launchArguments = ["-VisualTestMode", "1"]
app.launch()
let ready = app.descendants(matching: .any)[readyIdentifier]
XCTAssertTrue(ready.waitForExistence(timeout: 15))
let image = XCUIScreen.main.screenshot()
let attachment = XCTAttachment(screenshot: image)
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)
}
测试模式应关闭轮播、骨架动画和闪烁光标,并注入固定日期、用户名与列表数据。这里固定的是输入,不是把待测界面替换成另一套实现。若页面依赖请求完成,可在界面上暴露稳定的加载完成标识,而不是继续增加休眠时间。
处理字体与动画
字体必须来自系统或随应用交付,不能依赖某次人工安装。动画可通过启动参数关闭,截图前还要确认滚动已经停止。对于持续变化的视频、地图或计时器,优先替换为确定性测试夹具;只有无法控制的极小区域才使用遮罩。
设定能解释的像素阈值
PNG 文件字节不同不等于画面不同,直接用 cmp 判断容易受到编码信息影响。比较器应先把两张图解码为相同尺寸和 sRGB 像素,再逐像素计算通道差异。
建议同时使用两个条件:
- 单通道最大容差,例如允许轻微抗锯齿变化。
- 超过容差的像素占比,例如不得超过整张图的极小比例。
只用平均差异会掩盖局部严重问题:一个消失的按钮可能只占整图很小面积。比较器还应输出高亮差异图,并记录超限像素数量、边界框和阈值。关键确认页可以使用严格规则,包含阴影或复杂渐变的页面则单独配置,但不要为了“变绿”不断放宽全局阈值。
把失败变成可复查证据
每次失败至少归档基准图、实际图、差异图和 .xcresult,同时写入提交标识、Xcode 版本、系统运行时、设备型号、语言与外观。文件名应由测试套件、页面状态和环境组成,避免并行任务互相覆盖。
排查时按顺序检查:先确认尺寸和运行时是否变化,再看测试数据是否稳定,然后检查差异边界框。若差异遍布全屏,通常是字体、外观或色彩空间漂移;若只集中在一个控件,才更可能是布局修改。
基准更新也应走独立步骤:生成候选图、由人工查看差异、确认变更意图后再替换。测试任务不应在失败时自动覆盖基准,否则真正的回归会被下一次运行直接接受。
完成后,这套门禁应具备三个特征:环境可以重建,阈值可以解释,失败可以复现。做到这三点,截图测试才是工程检查,而不是偶尔需要人工清理的图片仓库。
常见问题
视觉回归测试应该使用完全相同的像素作为通过条件吗?
通常不应该。建议同时设置单通道差异阈值和超限像素占比,并为动画、光标等动态区域建立小范围遮罩;登录页、支付确认等稳定关键页面可以采用更严格的阈值。
为什么同一提交在不同模拟器上会产生截图差异?
设备尺寸、系统版本、显示缩放、字体、语言、时区和状态栏都会改变渲染结果。基准图必须绑定明确的设备型号与系统运行时,不能跨环境共用。
视觉回归失败后至少要保留哪些文件?
至少保留基准图、实际图、差异图、测试结果包、提交标识、模拟器型号和系统版本。仅保存一张失败截图通常不足以判断是产品变化还是环境漂移。
把已验证的工作流放到持续在线的云端 Mac
按用途选择 BookaMac M4 或 BookaMac M4 Pro,并在下单时核对地区、租期与存储附加项。