BookaMac 工程记录

云端 Mac 上建立 iOS 视觉回归门禁

云端 Mac 上建立 iOS 视觉回归门禁

同一段 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 像素,再逐像素计算通道差异。

建议同时使用两个条件:

  1. 单通道最大容差,例如允许轻微抗锯齿变化。
  2. 超过容差的像素占比,例如不得超过整张图的极小比例。

只用平均差异会掩盖局部严重问题:一个消失的按钮可能只占整图很小面积。比较器还应输出高亮差异图,并记录超限像素数量、边界框和阈值。关键确认页可以使用严格规则,包含阴影或复杂渐变的页面则单独配置,但不要为了“变绿”不断放宽全局阈值。

把失败变成可复查证据

每次失败至少归档基准图、实际图、差异图和 .xcresult,同时写入提交标识、Xcode 版本、系统运行时、设备型号、语言与外观。文件名应由测试套件、页面状态和环境组成,避免并行任务互相覆盖。

排查时按顺序检查:先确认尺寸和运行时是否变化,再看测试数据是否稳定,然后检查差异边界框。若差异遍布全屏,通常是字体、外观或色彩空间漂移;若只集中在一个控件,才更可能是布局修改。

基准更新也应走独立步骤:生成候选图、由人工查看差异、确认变更意图后再替换。测试任务不应在失败时自动覆盖基准,否则真正的回归会被下一次运行直接接受。

完成后,这套门禁应具备三个特征:环境可以重建,阈值可以解释,失败可以复现。做到这三点,截图测试才是工程检查,而不是偶尔需要人工清理的图片仓库。

常见问题

视觉回归测试应该使用完全相同的像素作为通过条件吗?

通常不应该。建议同时设置单通道差异阈值和超限像素占比,并为动画、光标等动态区域建立小范围遮罩;登录页、支付确认等稳定关键页面可以采用更严格的阈值。

为什么同一提交在不同模拟器上会产生截图差异?

设备尺寸、系统版本、显示缩放、字体、语言、时区和状态栏都会改变渲染结果。基准图必须绑定明确的设备型号与系统运行时,不能跨环境共用。

视觉回归失败后至少要保留哪些文件?

至少保留基准图、实际图、差异图、测试结果包、提交标识、模拟器型号和系统版本。仅保存一张失败截图通常不足以判断是产品变化还是环境漂移。

独享物理 Mac mini

把已验证的工作流放到持续在线的云端 Mac

按用途选择 BookaMac M4 或 BookaMac M4 Pro,并在下单时核对地区、租期与存储附加项。

选择租用方案