在雲端 Mac 上用 xccov 建立 iOS 程式碼覆蓋率回歸門檻

在雲端 Mac 上用 xccov 建立 iOS 程式碼覆蓋率回歸門檻

團隊將一批單元測試遷移到雲端 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 還原執行參數與工具鏈

只上傳百分比截圖,無法回答「實際執行了哪些測試」;原始結果套件才是問題排查的依據。

同時設定兩層門檻

專案整體覆蓋率適合用來發現大幅倒退,但大型程式碼庫新增數十行未測試的程式碼時,整體數值可能只變化零點幾個百分點。因此,建議採用兩層規則:

  1. 專案整體覆蓋率不得低於固定底線。
  2. 本次異動涉及的可執行原始碼檔案不得低於更高門檻。

例如,將整體覆蓋率底線設為 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 檔案直接納入門檻;以下內容通常需要明確排除:

排除規則應存放在儲存庫中並接受程式碼審查,不能在失敗後臨時新增萬用字元。比較路徑前,還應統一儲存庫根目錄、解析符號連結,並移除暫存建置目錄的前綴,否則同一檔案可能因絕對路徑不同而無法配對。

對於重新命名的檔案,使用 git diff --name-status -M 會比單純的檔案清單更可靠。若檔案從舊路徑移至新路徑,應依新路徑尋找報告項目,而不是將其誤判為「報告中不存在」。

處理平行測試與波動

覆蓋率偶發變化通常不是 xccov 本身具有隨機性,而是測試執行結果不確定。非同步測試應等待明確狀態,不能依賴固定秒數的休眠;共用資料庫、單例與暫存目錄都應在每個測試前重設。若平行測試會爭用同一資源,可先對相關測試目標停用平行執行,再找出具體的共用點。

模擬器也需要具備可追蹤的生命週期。長駐執行器可在任務開始前清除應用程式資料,但不必每次刪除所有執行階段。真正需要比較的是,同一測試計畫在相同工具鏈下能否連續得到一致結果。可以先對同一次提交重複執行三次,記錄各檔案的差值;持續波動的檔案應先修復測試隔離問題,再納入嚴格門檻。

最後,應將失敗摘要寫成可讓人直接採取行動的資訊:實際整體覆蓋率、要求值、低於門檻的異動檔案、該檔案已覆蓋與可執行的行數,以及原始 xcresult 的封存位置。如此一來,開發者不必重新執行整套任務,就能判斷應該補充測試、修正指令碼,還是處理測試環境的不確定性。

常見問題

程式碼覆蓋率門檻只需要檢查專案總覆蓋率嗎?

不需要。總覆蓋率適合防止整體大幅倒退,但對小型提交不夠敏感;異動檔案覆蓋率更能反映本次變更。兩者應同時檢查,並明確排除產生式程式碼與測試輔助檔案。

為什麼相同提交的覆蓋率有時會不同?

通常是測試範圍、非同步工作、平行測試共享狀態、模擬器殘留資料或 Xcode 版本不同所致。先固定 scheme、destination、測試計畫與工具鏈,再處理測試的不確定性。

覆蓋率門檻失敗時應保存哪些產物?

至少保存原始 xcresult、coverage.json、門檻判定摘要、完整測試命令與提交識別碼,才能區分測試未執行、解析失敗和真實覆蓋率下降。

獨享物理節點

用一台可依專案啟停的雲端 Mac 重現工作流程

從三種 Apple Silicon 配置中選擇合適的資源,按日、週、月或季租用。設備為獨享物理機,並非虛擬機器;實際可用狀態以控制台即時回傳的資訊為準。

選擇配置並下單