團隊將單元測試、介面測試與 UI 測試拆分到多個 Xcode Test Plan 後,最常見的問題往往不是測試失敗,而是測試根本沒有依照預期執行:有人在本機 Scheme 中切換了預設計畫,有人暫時略過某個測試案例後便提交檔案,也有人修改了環境變數,卻未同步更新流水線。雲端 Mac 會如實執行儲存庫中的設定,因此第一步不是增加重試次數,而是將測試計畫本身視為需要審查的程式碼。
先固定唯一且可追蹤的入口
Scheme 必須是共享檔案,路徑通常為 App.xcodeproj/xcshareddata/xcschemes/App-CI.xcscheme。不要依賴使用者目錄下的 xcuserdata,其中的設定無法穩定納入版本控制。
在 Scheme 的 Test Action 中引用儲存庫內的 App-CI.xctestplan,再讓流水線明確傳入計畫名稱。建置節點開始工作時,先檢查該計畫是否可見:
set -euo pipefail
xcodebuild \
-workspace App.xcworkspace \
-scheme App-CI \
-showTestPlans
xcodebuild \
-workspace App.xcworkspace \
-scheme App-CI \
-testPlan App-CI \
-destination 'platform=iOS Simulator,name=iPhone 16' \
test
-testPlan 不可省略。否則一旦 Scheme 的預設值發生變化,命令仍可能成功,但實際執行的測試集合可能已經不同。
將計畫檔案拆解成可核對的契約
.xctestplan 是 JSON 檔案,很適合進行靜態檢查。門檻至少應涵蓋四類欄位:
| 檢查項目 | 應有規則 | 失敗代表的意義 |
|---|---|---|
| configurations | 必須包含 CI 設定 | 流水線入口遭到刪除或重新命名 |
| testTargets | 必須包含約定的目標 | 某組測試未被執行 |
| skippedTests | 只能符合允許清單 | 新增了未經說明的略過項目 |
| environmentVariableEntries | 禁止敏感值,且關鍵鍵名必須存在 | 環境發生漂移,或資訊進入儲存庫 |
不要直接比較整份 JSON 的文字順序。Xcode 可能重新排列陣列或補充欄位,逐字比較只會產生沒有意義的失敗。應解析其結構,僅驗證團隊真正依賴的約束。
測試計畫門檻的目的並非阻止任何修改,而是讓「少執行了哪些測試、變更了哪些環境設定」在合併前清楚可見。
使用指令碼攔截目標遺漏與略過項目
以下指令碼會檢查 CI 設定、必要的測試目標,以及未登記的略過項目。允許清單應保持精簡,並要求在程式碼審查中說明移除條件。
#!/usr/bin/env python3
import json
import sys
from pathlib import Path
plan = json.loads(Path("App-CI.xctestplan").read_text())
required_targets = {"AppTests", "AppIntegrationTests"}
allowed_skips = {
"AppIntegrationTests/testTemporaryServerResponse"
}
config_names = {item["name"] for item in plan.get("configurations", [])}
if "CI" not in config_names:
sys.exit("Missing CI test configuration")
targets = plan.get("testTargets", [])
target_names = {
item.get("target", {}).get("name")
for item in targets
}
missing = required_targets - target_names
if missing:
sys.exit(f"Missing test targets: {sorted(missing)}")
actual_skips = {
test
for item in targets
for test in item.get("skippedTests", [])
}
unexpected = actual_skips - allowed_skips
if unexpected:
sys.exit(f"Unapproved skipped tests: {sorted(unexpected)}")
請在測試命令之前執行此指令碼。若檢查失敗,不應自動改寫計畫檔案,因為自動修正可能掩蓋開發者刻意進行的設定變更。
為允許清單設定退出條件
允許清單不能變成永久的垃圾桶。每個項目至少都應對應一筆缺陷記錄、負責人及刪除條件。若團隊不想再維護另一份結構化檔案,可以在程式碼審查範本中強制填寫這些資訊,並定期執行指令碼輸出目前的清單。
分離穩定變數與流水線機密
測試計畫適合保存不會隨節點變動的開關,例如 UITEST_MODE=1、固定語言或模擬服務模式。存取權杖、私密金鑰與一次性憑證不應寫入計畫檔案,也不應以明文參數形式放入 Scheme。
雲端 Mac 的流水線可以在執行前注入機密,並讓測試程序讀取環境變數。審計指令碼則只驗證鍵名是否存在,以及值是否來自允許的固定集合。如此既能確保計畫可重現,也不會讓敏感資訊混入儲存庫。
還要留意變數展開所指向的目標。如果計畫引用了已重新命名的 Target,Xcode 介面或許仍可開啟檔案,但執行階段的變數解析可能偏離預期。重新命名專案時,應將計畫檢查與 xcodebuild -list 一併執行。
讓失敗保留足夠的證據
通過門檻後再執行完整測試,並將結果套件寫入固定目錄:
rm -rf artifacts/App-CI.xcresult
xcodebuild \
-workspace App.xcworkspace \
-scheme App-CI \
-testPlan App-CI \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-resultBundlePath artifacts/App-CI.xcresult \
test
失敗時應保留三項資訊:目前的 .xctestplan、實際執行的命令,以及 .xcresult。只保存主控台末尾的日誌,不足以判斷究竟是測試邏輯失敗、目標未載入,還是計畫設定發生變化。
在 ZoneMini 上執行固定測試節點時,也應在每次工作開始前重新執行靜態審計,而不是假設長期運作的環境必然一致。共享 Scheme 確定入口,測試計畫描述集合,審計指令碼約束變更,結果套件保存證據;唯有四層同時存在,「測試通過」才會具有穩定且一致的意義。
常見問題
為什麼不能只在 CI 固定一條 xcodebuild 指令?
指令只能指定 Scheme 與測試計畫名稱,無法防止計畫檔內的測試目標、略過項目及環境變數被改動,因此還要稽核 .xctestplan 與共享 Scheme。
測試計畫內的 skippedTests 是否都該禁止?
不需要全面禁止。確有需要的暫時略過項目應放入包含負責人、原因及截止條件的允許清單,未登記的新項目則阻擋合併。
如何避免開發機與雲端 Mac 選到不同測試計畫?
把 Scheme 存入 xcshareddata/xcschemes,明確引用版本庫中的 .xctestplan,並在 CI 固定 -testPlan 參數,再用 -showTestPlans 驗證。
為持續建置工作選擇獨享雲端 Mac
確認 M4、記憶體、儲存空間、節點與計費週期,再將固定建置環境接入現有工作流程。