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,下單時請確認地區、租用期限及儲存空間加購項目。

選擇租用方案