Runnerエンジニアリング記事

クラウドMacでiOSデータのバックアップ境界を回帰テストする

クラウドMacでiOSデータのバックアップ境界を回帰テストする

あるニュース系Appでは、バージョンアップ後、再ダウンロード可能な数百MBのオフラインリソースが Application Support に保存されるようになりました。機能テストはすべて合格し、ユーザーデータも失われていませんでしたが、バックアップ容量が突然増加しました。問題はダウンロード処理ではなく、「復元が必須のファイル」と「再構築できるファイル」を、チームが実行可能なルールとして定義していなかったことです。クラウドMacは、この種の回帰チェックに適しています。環境を長期間一貫した状態に保て、シミュレータをリセットできるうえ、失敗時には調査用のコンテナ一覧も保存できます。

まずデータの復元要件を定義する

最初からディレクトリ単位でテストを書くのではなく、まずデータの用途に基づいて分類します。同じJSONファイルでも、ユーザーが作成したデータの場合もあれば、単なるAPIキャッシュの場合もあります。それぞれに必要なバックアップ方針はまったく異なります。

データ種別 推奨保存先 バックアップ要件 例
ユーザーが作成し、再構築できないデータ Documents 保持する 下書き、ユーザーがインポートしたファイル
Appの永続的な状態 Application Support ビジネス要件に応じて判断 データベース、編集の進捗
再生成または再ダウンロードできるデータ Library/Caches バックアップに依存しない 画像キャッシュ、オフラインインデックス
単発タスクの中間ファイル tmp 保持しない 展開先ディレクトリ、分割ファイル

Application Support は特に誤用されやすい保存先です。Appが長期的に管理するデータには適していますが、そこにあるすべての内容をバックアップすべきとは限りません。大型モデル、地図パッケージ、メディアのプロキシファイルなどを再取得できる場合は、バックアップ除外属性を明示的に設定するか、Caches に移す必要があります。

判断基準は「このファイルは重要か」ではなく、「デバイスの復元後、このファイルをバックアップから戻す必要があり、かつ信頼できる取得元から再構築できないか」です。

ルールはリポジトリ内の一覧として整理し、論理名、相対パス、復元要件、担当者などを記録します。テストの役割は、この合意済みのルールを実行することです。コード内に散在する偶発的なパスを、そのまま仕様として扱うべきではありません。

XCTestでバックアップ除外属性を固定する

Foundationには、低レベルの拡張属性を直接読み取るよりも安定したAPIが用意されています。次のヘルパーメソッドでは、ファイルの作成後に 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 の増加は許容しても、ユーザーの下書きに使う拡張子のファイルが含まれていてはいけません。大型のテストリソースには明示的な許可リストを使用し、失敗メッセージには相対パス、ファイルサイズ、該当ルール、作成された処理段階を出力します。

失敗時の状態をすぐに削除しない

失敗後、最初にシミュレータを消去してはいけません。まず、次の情報をアーカイブします。

これらの証跡があれば、「属性が設定されていない」「保存先ディレクトリが間違っている」「クリーンアップ処理が実行されていない」という問題を切り分けられます。添付ファイルの保存が完了してからデバイスを削除し、断続的な失敗の証拠が1行のアサーションだけにならないようにします。

安定したCIゲートとして設計する

バックアップ境界テストは、ストレージ層、ダウンローダー、データベース移行、エクスポート処理を変更するたびに実行します。マージ前の軽量テストセットに組み込むこともできます。外部サービスを必要とするエンドツーエンドフローに結び付けてはいけません。ローカルフィクスチャと固定サイズのデータを使うことで、初めて再現性のある結果が得られます。

失敗は3種類に分類することを推奨します。必ずブロックすべきユーザーデータの保存先エラー、同じく必ずブロックすべき除外属性の欠落、警告にとどめる容量トレンドの変化です。容量のしきい値はリポジトリ内の設定で管理し、変更理由をレビューで説明する必要があります。スクリプトが上限超過を検出した後、自動的にしきい値を緩和してはいけません。

最後に、実機での受け入れ確認を残します。シミュレータではディレクトリ、属性、移行コードを安定して検証できますが、実際の復元フローにおけるすべての挙動を網羅することはできません。リリース確認では最小限のデータセットを選び、再構築できないデータが復元されること、再生成可能なデータが復元の前提になっていないことを確認します。こうすることで、自動化されたゲートが高頻度の回帰検査を担い、実機での手順が最終的な境界を検証します。両者は互いを代替するものではありません。

よくある質問

通常はバックアップから除外すべきiOSファイルは何ですか?

再取得できるキャッシュ、オフライン素材、展開途中のファイル、一時的な書き出し結果は、Cachesまたはtmpへ置くか、isExcludedFromBackupを明示的に設定します。

保存ディレクトリの確認だけでは不十分なのはなぜですか?

移行処理や外部ライブラリが誤った場所へ保存する可能性があるため、用途、除外属性、再インストール後に復元すべきかどうかも検査する必要があります。

シミュレータ検査だけで実機確認を省略できますか?

できません。シミュレータは保存先と属性の継続検査に適していますが、公開前には管理された実機手順でバックアップ、復元、データ保護の挙動を確認します。

専用物理ノード

クラウド上のM4 Macで次の作業を実行

Runner M4またはRunner M4 Plusを選び、プロジェクトに合わせてノード、利用期間、ストレージの追加オプションを指定できます。各注文には専用の物理マシンが割り当てられ、仮想マシンではありません。

クラウドMacを今すぐレンタル