團隊將一批單元測試遷移到雲端 Mac 後,最容易犯的錯誤就是只關注「測試是否通過」。某次提交可能刪除了關鍵分支的斷言,測試依然全部通過,覆蓋率卻悄悄下降。更穩妥的做法,是讓每次建置產生獨立的 xcresult,再使用 Xcode 內建的 xccov 擷取資料,同時檢查專案整體覆蓋率與本次異動的檔案。
先固定覆蓋率的輸入條件
只有在測試入口穩定的情況下,覆蓋率才值得比較。首先固定 Xcode 版本、scheme、測試計畫、模擬器機型與系統版本;不要讓不同執行器自行選擇 destination。測試命令也應明確啟用覆蓋率,並為每次執行建立新的結果目錄。
set -euo pipefail
RESULT_DIR="$PWD/Artifacts/Coverage"
RESULT_BUNDLE="$RESULT_DIR/TestResults.xcresult"
rm -rf "$RESULT_BUNDLE"
mkdir -p "$RESULT_DIR"
xcodebuild test \
-workspace ExampleApp.xcworkspace \
-scheme ExampleApp-CI \
-testPlan UnitTests \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-enableCodeCoverage YES \
-resultBundlePath "$RESULT_BUNDLE"
resultBundlePath 指向的路徑在執行前必須不存在,否則舊結果可能會阻止寫入。CI 指令碼還應記錄 xcodebuild -version、目前的提交識別碼與完整 destination,避免將工具鏈變更誤判為程式碼回歸。
覆蓋率門檻衡量的是同一組測試條件下的變化,而不是不同模擬器、不同測試計畫之間的橫向排名。
從 xcresult 擷取可稽核的資料
測試完成後,應使用 xccov 匯出 JSON,而不是解析終端機中的格式化文字。JSON 更適合保留檔案路徑、目標名稱與行覆蓋率,也不會因顯示寬度變化而造成欄位錯位。
xcrun xccov view \
--report \
--json \
"$RESULT_BUNDLE" > "$RESULT_DIR/coverage.json"
test -s "$RESULT_DIR/coverage.json"
解析器應先確認 targets 存在,再依目標彙整檔案。若報告為空,不要將其視為 0% 覆蓋率後繼續比較,而應直接標記為擷取失敗。常見原因包括 scheme 未啟用測試、目標未納入覆蓋率統計,或測試程序在報告寫入磁碟前異常結束。
保留原始證據
建議將以下內容儲存為同一次任務的產物:
| 產物 | 用途 |
|---|---|
TestResults.xcresult |
複核測試、日誌與覆蓋率來源 |
coverage.json |
供指令碼穩定解析 |
coverage-summary.json |
儲存門檻、實際值與失敗檔案 |
command.txt |
還原執行參數與工具鏈 |
只上傳百分比截圖,無法回答「實際執行了哪些測試」;原始結果套件才是問題排查的依據。
同時設定兩層門檻
專案整體覆蓋率適合用來發現大幅倒退,但大型程式碼庫新增數十行未測試的程式碼時,整體數值可能只變化零點幾個百分點。因此,建議採用兩層規則:
- 專案整體覆蓋率不得低於固定底線。
- 本次異動涉及的可執行原始碼檔案不得低於更高門檻。
例如,將整體覆蓋率底線設為 72%,異動檔案設為 85%。門檻應以團隊目前的基準線為依據,而不是一次就設定成無法落實的理想值。以下 Python 程式碼片段示範如何讀取目標覆蓋率;實際專案可進一步展開 files,逐一判斷各檔案。
import json
import sys
with open("Artifacts/Coverage/coverage.json", encoding="utf-8") as f:
report = json.load(f)
targets = report.get("targets", [])
if not targets:
raise SystemExit("Coverage report has no targets")
tested = [t for t in targets if t.get("name") == "ExampleApp.app"]
if len(tested) != 1:
raise SystemExit("Expected application target was not found")
coverage = float(tested[0]["lineCoverage"]) * 100
minimum = 72.0
print(f"application_line_coverage={coverage:.2f}")
sys.exit(0 if coverage >= minimum else 2)
指令碼應使用不同的結束代碼,區分「覆蓋率未達標」與「報告無法解析」。前者需要補充測試或說明變更,後者則應修復資料擷取流程,不能將兩者混為同一種失敗。
只檢查真正需要測試的檔案
可以透過 git diff --name-only 取得異動檔案,再與 xccov 報告中的路徑取交集。不要將所有 .swift 檔案直接納入門檻;以下內容通常需要明確排除:
- 自動產生的資源存取器與介面用戶端;
Tests目錄中的測試輔助程式碼;- 只有宣告、沒有可執行邏輯的模型檔案;
- 由建置工具產生且不會由人工維護的原始碼。
排除規則應存放在儲存庫中並接受程式碼審查,不能在失敗後臨時新增萬用字元。比較路徑前,還應統一儲存庫根目錄、解析符號連結,並移除暫存建置目錄的前綴,否則同一檔案可能因絕對路徑不同而無法配對。
對於重新命名的檔案,使用 git diff --name-status -M 會比單純的檔案清單更可靠。若檔案從舊路徑移至新路徑,應依新路徑尋找報告項目,而不是將其誤判為「報告中不存在」。
處理平行測試與波動
覆蓋率偶發變化通常不是 xccov 本身具有隨機性,而是測試執行結果不確定。非同步測試應等待明確狀態,不能依賴固定秒數的休眠;共用資料庫、單例與暫存目錄都應在每個測試前重設。若平行測試會爭用同一資源,可先對相關測試目標停用平行執行,再找出具體的共用點。
模擬器也需要具備可追蹤的生命週期。長駐執行器可在任務開始前清除應用程式資料,但不必每次刪除所有執行階段。真正需要比較的是,同一測試計畫在相同工具鏈下能否連續得到一致結果。可以先對同一次提交重複執行三次,記錄各檔案的差值;持續波動的檔案應先修復測試隔離問題,再納入嚴格門檻。
最後,應將失敗摘要寫成可讓人直接採取行動的資訊:實際整體覆蓋率、要求值、低於門檻的異動檔案、該檔案已覆蓋與可執行的行數,以及原始 xcresult 的封存位置。如此一來,開發者不必重新執行整套任務,就能判斷應該補充測試、修正指令碼,還是處理測試環境的不確定性。
常見問題
程式碼覆蓋率門檻只需要檢查專案總覆蓋率嗎?
不需要。總覆蓋率適合防止整體大幅倒退,但對小型提交不夠敏感;異動檔案覆蓋率更能反映本次變更。兩者應同時檢查,並明確排除產生式程式碼與測試輔助檔案。
為什麼相同提交的覆蓋率有時會不同?
通常是測試範圍、非同步工作、平行測試共享狀態、模擬器殘留資料或 Xcode 版本不同所致。先固定 scheme、destination、測試計畫與工具鏈,再處理測試的不確定性。
覆蓋率門檻失敗時應保存哪些產物?
至少保存原始 xcresult、coverage.json、門檻判定摘要、完整測試命令與提交識別碼,才能區分測試未執行、解析失敗和真實覆蓋率下降。
用一台可依專案啟停的雲端 Mac 重現工作流程
從三種 Apple Silicon 配置中選擇合適的資源,按日、週、月或季租用。設備為獨享物理機,並非虛擬機器;實際可用狀態以控制台即時回傳的資訊為準。