一款資訊類 App 在版本更新後,將數百 MB 可重新下載的離線資源放進了 Application Support。功能測試全數通過,使用者資料也沒有遺失,但備份容量卻突然增加。問題不在下載邏輯,而在於團隊從未將「哪些檔案必須還原、哪些檔案可以重建」寫成可執行的規則。雲端 Mac 很適合承擔這類回歸檢查:環境能長期維持一致、模擬器可以重設,失敗時也能保留容器清單以供排查。
先定義資料的還原語意
不要一開始就按照目錄撰寫測試,而應先依資料用途分類。同一個 JSON 檔案可能是使用者創作的內容,也可能只是 API 快取;兩者的備份要求完全不同。
| 資料類型 | 建議位置 | 備份預期 | 範例 |
|---|---|---|---|
| 使用者建立且無法重建 | Documents |
應保留 | 草稿、使用者匯入的檔案 |
| App 持久狀態 | Application Support |
依業務需求決定 | 資料庫、編輯進度 |
| 可重新產生或下載 | Library/Caches |
不依賴備份 | 圖片快取、離線索引 |
| 單次任務的中間檔案 | tmp |
不保留 | 解壓縮目錄、分段檔案 |
Application Support 最容易被誤用。它適合存放由 App 長期管理的資料,但不代表其中所有內容都應納入備份。大型模型、地圖套件或媒體代理檔案如果可以重新取得,就應明確設定排除屬性,或改放到 Caches。
判斷標準不是「這個檔案重要嗎」,而是「裝置還原後,這個檔案是否必須從備份中回來,而且無法從可靠來源重建」。
將規則整理成一份存放於儲存庫內的清單,例如記錄邏輯名稱、相對路徑、還原要求與負責人。測試只負責執行這份約定,不應將散落在程式碼中的偶然路徑當成規範。
使用 XCTest 固定排除屬性
Foundation 提供了比讀取底層延伸屬性更穩定的介面。以下輔助方法會在建立檔案後設定 isExcludedFromBackup,再重新讀取資源值,確認設定已實際寫入。
import XCTest
final class BackupBoundaryTests: XCTestCase {
private func markExcluded(_ url: URL) throws {
var values = URLResourceValues()
values.isExcludedFromBackup = true
var mutableURL = url
try mutableURL.setResourceValues(values)
}
func testDownloadPackageIsExcludedFromBackup() throws {
let root = FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
)[0]
let package = root.appendingPathComponent(
"Downloads/catalog.bundle",
isDirectory: false
)
try FileManager.default.createDirectory(
at: package.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try Data("fixture".utf8).write(to: package)
try markExcluded(package)
let values = try package.resourceValues(
forKeys: [.isExcludedFromBackupKey]
)
XCTAssertEqual(values.isExcludedFromBackup, true)
}
}
測試重點不是證明 Foundation 能運作,而是確保正式環境的程式碼在建立檔案後,也會經過同一條設定路徑。更穩妥的做法,是將目錄建立、原子寫入與排除標記封裝到儲存元件中,再由測試直接呼叫該元件。否則,即使測試程式碼成功設定屬性,正式環境的下載器仍可能漏掉設定,這道門禁依然沒有價值。
同時檢查錯誤放置
還要加入反向斷言:使用者草稿不得進入 Caches,暫時匯出的產物也不能留在 Documents。測試固定資料應涵蓋建立、升級遷移與失敗重試,因為路徑偏移經常發生在遷移分支,而不是首次安裝流程。
如果是目錄層級的排除,也要抽查新建立的子檔案。不要假設父目錄目前的屬性能永久取代寫入邏輯;元件每次建立目錄後都主動確認屬性,才能更有效地抵抗舊資料遷移與目錄重建造成的影響。
在模擬器中保留容器證據
單元測試適合驗證規則,模擬器容器檢查則適合回答「失敗時實際寫入了什麼」。先讓測試使用固定裝置與獨立的 DerivedData,避免多個任務共用狀態。
set -euo pipefail
DERIVED_DATA="$PWD/.ci/DerivedData-backup"
RESULT_BUNDLE="$PWD/.ci/BackupBoundary.xcresult"
rm -rf "$DERIVED_DATA" "$RESULT_BUNDLE"
xcodebuild test \
-workspace Example.xcworkspace \
-scheme Example \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-derivedDataPath "$DERIVED_DATA" \
-resultBundlePath "$RESULT_BUNDLE"
APP_DATA="$(
xcrun simctl get_app_container booted com.example.app data
)"
find "$APP_DATA" -type f -print | LC_ALL=C sort \
> "$PWD/.ci/app-container-files.txt"
get_app_container 只有在目標 App 已安裝,而且對應的模擬器處於啟動狀態時才會成功。CI 不應默默選擇任意已啟動的裝置;應在任務開始時建立或指定裝置、等待啟動完成,並在任務結束後關閉。若在 Runner 上平行執行多組測試,每個任務應使用獨立的模擬器裝置集,避免容器互相串接。
檔案清單只記錄相對路徑會更安全,也更容易比較。不要將家目錄、工作區絕對路徑或測試憑證寫入產物。若需要檢查延伸屬性,可以將 xattr 輸出作為診斷附件,但門禁結論仍應以 Foundation 資源值與業務清單為準,避免依賴未受保證的底層行為。
將容量異常變成可解釋的失敗
只檢查布林屬性仍然不夠。一次路徑重構可能讓所有快取都進入 Documents,但每個檔案本身都沒有對應的排除屬性測試。可以為容器的頂層目錄設定容量預算,但預算應反映結構異常,而不是追求固定的位元組數。
例如,執行測試固定資料後,要求 Documents 只包含指定樣本;任務結束時 tmp 必須為空;Caches 可以成長,但其中不得出現使用者草稿的副檔名。大型測試資源應使用明確的允許清單,並在失敗訊息中輸出相對路徑、檔案大小、所屬規則與建立階段。
不要直接刪除失敗現場
失敗後的第一個動作不應是清空模擬器。請先封存以下內容:
- XCTest 結果套件與失敗測試案例名稱;
- App 容器的相對路徑清單;
- 各頂層目錄的彙總大小;
- 測試所使用的提交編號、Scheme 與模擬器執行階段;
- 觸發檔案建立的測試步驟。
這些證據足以區分「屬性未設定」、「檔案放錯目錄」與「清理流程未執行」。確認附件收集完成後再刪除裝置,避免偶發失敗最後只剩下一行斷言。
設計成穩定的 CI 門禁
每次修改儲存層、下載器、資料庫遷移或匯出流程時,都應執行備份邊界測試;也可以將其納入合併前的輕量測試集。不要把它綁定到需要外部服務的端對端流程;只有使用本機固定資料與固定大小的資料,結果才能重複重現。
建議將失敗分成三類:必須阻擋的使用者資料位置錯誤、必須阻擋的排除屬性缺失,以及只發出警告的容量趨勢變化。容量門檻應由儲存庫內的設定控制,並在審查中說明調整原因,不能讓指令稿在發現超出限制後自動放寬。
最後仍要保留一次實機驗收。模擬器可以穩定驗證目錄、屬性與遷移程式碼,卻無法涵蓋實際還原流程的全部行為。發布檢查應選取一份最小資料集,確認無法重建的資料可以還原,而可再生資料不會被當成還原的前提。如此一來,自動化門禁負責高頻回歸,實機流程負責驗證最終邊界,兩者不會互相取代。
常見問題
哪些 iOS 檔案通常不應進入備份?
可重新下載或重新產生的快取、離線資源包、解壓縮中間檔與暫存匯出物通常不應備份,應放入 Caches、tmp,或明確設定 isExcludedFromBackup。
為什麼不能只檢查檔案所在目錄?
目錄只代表預設語意,遷移程式、外部套件或舊版本可能把檔案放錯位置,因此還要檢查用途、排除屬性與重新安裝後的還原需求。
模擬器檢查可以完全取代實機驗證嗎?
不可以。模擬器適合持續檢查目錄與屬性規則,發佈前仍應在受控實機流程中驗證實際備份、還原與資料保護行為。
在雲端 M4 Mac 上執行下一項工作
選擇 Runner M4 或 Runner M4 Plus,依專案需求決定節點、租期與儲存空間附加選項。每筆訂單皆對應一台獨享實體機,並非虛擬機。