Runner 工程文章

在雲端 Mac 用 simctl 建立 iOS 剪貼簿回歸測試

在雲端 Mac 用 simctl 建立 iOS 剪貼簿回歸測試

遠端執行 iOS 測試時,剪貼簿相關功能很容易落入「在本機點一下就能用,卻無法在管線中驗證」的盲區。匯入邀請碼、貼上除錯指令、清理多行文字,以及應用程式內複製等功能都依賴系統剪貼簿,但一般單元測試往往會繞過真實入口。更穩妥的做法是為每項工作配置獨立模擬器,使用 simctl 注入固定文字,再由 UI 測試執行使用者可見的貼上操作。

先定義測試邊界

這套流程適合驗證純文字、Unicode、空白字元與換行處理,不應將 pbcopy 當成任意二進位資料的注入工具。圖片、檔案 URL 或自訂 UTI 應由應用程式內的測試治具或專用測試宿主提供。

先列出真正需要保護的行為:

自動化的目標不是直接讀取系統剪貼簿,而是驗證使用者觸發複製或貼上後,應用程式呈現的結果是否正確。

為每項工作準備乾淨的模擬器

平行工作不能共用 booted 這種模糊目標。兩項工作同時執行 pbcopy 時,後寫入的內容會覆蓋先前內容。應由排程層傳入固定 UDID,並為 DerivedData 與結果套件配置獨立目錄。

set -euo pipefail

: "${IOS_UDID:?IOS_UDID is required}"
JOB_ROOT="${PWD}/artifacts/${CI_JOB_ID:-local}"
mkdir -p "$JOB_ROOT"

xcrun simctl shutdown "$IOS_UDID" 2>/dev/null || true
xcrun simctl erase "$IOS_UDID"
xcrun simctl boot "$IOS_UDID"
xcrun simctl bootstatus "$IOS_UDID" -b

erase 會清除應用程式容器與模擬器設定,適合追求確定性的驗收工作。若管線需要重複使用已安裝的應用程式來縮短準備時間,也不能省略清理設計:至少要解除安裝受測應用程式、清空剪貼簿,並記錄執行階段版本與 UDID。

隔離對象 建議做法 共用後的典型問題
模擬器 每項工作使用一個 UDID 剪貼簿與應用程式狀態互相覆蓋
DerivedData 依工作配置目錄 並行寫入造成建置結果不穩定
結果套件 每次產生新路徑 舊附件混入本次失敗記錄
文字治具 納入版本控制 輸入已變更,但測試程式碼未變更

注入可稽核的文字治具

不要直接在 Shell 中組合複雜的 Unicode 字串。將治具儲存為 UTF-8 檔案,才能在程式碼審查時看出輸入發生了哪些變化。

mkdir -p Tests/ClipboardFixtures

printf '第一行\n第二行:café\nemoji:🧪\n' \
  > Tests/ClipboardFixtures/multiline.txt

xcrun simctl pbcopy "$IOS_UDID" \
  < Tests/ClipboardFixtures/multiline.txt

治具應涵蓋正常輸入與邊界輸入,但不要把所有情況都塞進一個過長的檔案。建議分別維護單行、多行、前後空白、組合字元與空內容案例。測試失敗時,記錄只應包含治具檔名、位元組數與摘要,不要無條件輸出實際剪貼簿內容,因為其中可能含有敏感資料。

注入後可以在除錯階段讀取一次,確認問題出在注入端還是應用程式端:

xcrun simctl pbpaste "$IOS_UDID" > "$JOB_ROOT/clipboard-before.txt"
cmp Tests/ClipboardFixtures/multiline.txt \
  "$JOB_ROOT/clipboard-before.txt"

正式環境的管線不必長期保留這份原文。確認內容一致後刪除檔案,或只保留 shasum -a 256 的結果。

透過真實入口觸發貼上與複製

應用程式應提供使用者能辨識的貼上控制項,而不是在頁面載入時暗中讀取剪貼簿。UI 測試點按該控制項後再檢查介面結果,既能涵蓋互動路徑,也能避免將背景讀取誤判為功能正常。

func testPasteMultilineFixture() {
    let app = XCUIApplication()
    app.launchArguments += ["-ui-testing"]
    app.launch()

    let pasteButton = app.buttons["clipboard.paste"]
    XCTAssertTrue(pasteButton.waitForExistence(timeout: 10))
    pasteButton.tap()

    let editor = app.textViews["clipboard.editor"]
    XCTAssertTrue(editor.waitForExistence(timeout: 5))
    XCTAssertEqual(
        editor.value as? String,
        "第一行\n第二行:café\nemoji:🧪"
    )
}

如果系統顯示貼上確認介面,測試應將其視為互動的一部分,而不是透過內部 API 繞過。控制項的 accessibility identifier 必須保持穩定,不要依賴在地化後的按鈕文字。

複製方向可以反向驗證:UI 測試輸入固定內容並點按複製,測試結束後再使用 pbpaste 匯出結果。如此便能發現尾端換行、格式化文字降級,或錯誤的空白清理。

執行、蒐證與疑難排解

先完成建置,再執行測試,可減少「編譯失敗」與「互動失敗」混在同一份記錄中的情況。

xcodebuild build-for-testing \
  -scheme ClipboardApp \
  -destination "platform=iOS Simulator,id=$IOS_UDID" \
  -derivedDataPath "$JOB_ROOT/DerivedData"

xcodebuild test-without-building \
  -scheme ClipboardApp \
  -destination "platform=iOS Simulator,id=$IOS_UDID" \
  -derivedDataPath "$JOB_ROOT/DerivedData" \
  -resultBundlePath "$JOB_ROOT/ClipboardTests.xcresult"

失敗時依序檢查:UDID 是否屬於目前工作、模擬器是否完成啟動、治具雜湊是否一致、應用程式是否真的點按了貼上控制項,以及判斷式讀取的是否為最終介面狀態。不要一失敗就重複執行;重新執行可能掩蓋競爭條件,反而遺失現場資訊。

可以設定工作結束時執行清理,但應保留失敗時的 xcresult、模擬器記錄與螢幕截圖。剪貼簿原文預設應刪除,只留下長度、編碼、摘要與治具名稱。若測試涉及使用者可能輸入的內容,必須在封存前完成記錄去識別化。

將檢查項目固化到管線中

穩定的剪貼簿回歸工作至少應符合以下條件:

  1. UDID、DerivedData 與結果目錄均依工作隔離;
  2. 記錄模擬器執行階段版本;
  3. 文字治具以 UTF-8 檔案納入版本控制;
  4. 測試透過使用者操作觸發貼上或複製;
  5. 空剪貼簿、Unicode 與換行分別具備獨立案例;
  6. 失敗時保留介面證據,但不長期保留剪貼簿原文;
  7. 工作結束後清理模擬器中的暫時狀態。

這套測試的價值不在於涵蓋系統剪貼簿本身,而是將應用程式與剪貼簿之間的契約轉化為可重複驗收的輸入與輸出。當每次失敗都能對應到具體的 UDID、治具摘要、測試步驟與結果套件時,遠端疑難排解便不再依賴人工轉述。

常見問題

simctl 的 pbcopy 可以直接測試圖片剪貼簿嗎?

不應把它當成通用二進位剪貼簿注入器。文字適合使用 pbcopy 與 pbpaste;圖片、檔案 URL 和自訂 UTI 應透過應用程式內測試夾具或專用測試宿主注入。

平行工作可以共用同一個 iOS 模擬器嗎?

不建議。每個工作都應綁定獨立 UDID、DerivedData 與結果目錄,避免剪貼簿、應用程式容器和啟動狀態互相覆寫。

獨享實體節點

在雲端 M4 Mac 上執行下一項工作

選擇 Runner M4 或 Runner M4 Plus,依專案需求決定節點、租期與儲存空間附加選項。每筆訂單皆對應一台獨享實體機,並非虛擬機。

立即租用雲端 Mac