MiniRent エンジニアガイド

simctl privacyで作るiOS権限回帰テスト

simctl privacyで作るiOS権限回帰テスト

開発用Macでは成功するiOS UIテストが、クラウドMacへ移すと権限ダイアログで断続的に止まることがあります。多くの場合、アプリケーションコードが不規則に失敗しているのではなく、前回のジョブで設定されたTCC権限の状態がシミュレータに残っています。誰かが「許可」を選んでいれば、その後のテストでは初回確認が表示されません。逆に「許可しない」を選んでいれば、連絡先や写真に依存するフローは直ちに失敗側の分岐へ進みます。テストを安定させる鍵は、再試行の回数を増やすことではなく、各テストの開始前に権限の前提条件を明示することです。

権限状態をテスト入力として扱う

権限テストでは、少なくとも未決定、許可済み、拒否済みの3状態を網羅する必要があります。それぞれ異なるプロダクトフローに対応するため、すべてを「アプリを再インストールする」という1つの手順でまとめて扱うことはできません。

目標状態 simctlの操作 検証する動作
未決定 reset アプリがアクセスを要求し、システムが権限ダイアログを表示する
許可済み grant 再確認せず、機能がそのまま処理を続行する
拒否済み revoke アプリが代替動作の説明または設定画面への案内を表示する

最初に、使用するランタイムが対象サービスをサポートしているか確認します。制御できるサービスはシステムのバージョンによって異なる場合があるため、経験だけを頼りにサービス名をハードコードして、そのままコミットしてはいけません。

xcrun simctl privacy help
xcrun simctl list devices available

一般的なサービスには、contactsphotosphotos-addlocationmicrophonecalendarがあります。テストスクリプトでは、現在使用しているXcodeに付属するsimctlのヘルプ出力を基準にしてください。

権限状態はコードリポジトリではなく、シミュレータデバイスに属します。ビルドディレクトリを消去してもTCCデータは削除されません。また、アプリの削除と再インストールも、信頼できる権限リセット方法として扱うべきではありません。

専用のシミュレータとアプリを準備する

CIでは、起動中の任意のデバイスを指すためにbootedを安易に使用しないでください。並列ジョブが同じシミュレータを同時に取得すると、インストール、起動、権限変更が互いに上書きされる可能性があります。より確実なのは、ジョブごとに明確なUDIDを割り当て、スクリプトの引数として渡す方法です。

grantまたはrevokeを実際のBundle Identifierに対して有効にするには、先にアプリをインストールしておく必要があります。Bundle IdentifierをScheme名から推測せず、ビルド成果物から直接読み取ってください。

APP_PATH="$1"
UDID="$2"
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" \
  "$APP_PATH/Info.plist")

xcrun simctl boot "$UDID" 2>/dev/null || :
xcrun simctl bootstatus "$UDID" -b
xcrun simctl install "$UDID" "$APP_PATH"
printf '%s
' "$BUNDLE_ID"

拡張機能をテストする場合、その拡張機能には独立したBundle Identifierがあります。拡張機能のInfo.plistを別途読み取り、すべての権限コマンドでメインアプリの識別子を使い回さないでください。

3つの前提状態をスクリプトで固定する

状態の切り替えを1つの関数に集約すると、パイプライン設定内の各所に分散させるよりレビューしやすくなります。権限を変更する前にアプリを終了し、プロセスが古い状態を保持したままになったり、権限ダイアログを表示し続けたりしないようにします。

set -euo pipefail

UDID="$1"
BUNDLE_ID="$2"
SERVICE="$3"
STATE="$4"

xcrun simctl terminate "$UDID" "$BUNDLE_ID" 2>/dev/null || :

case "$STATE" in
  undecided)
    xcrun simctl privacy "$UDID" reset "$SERVICE" "$BUNDLE_ID"
    ;;
  authorized)
    xcrun simctl privacy "$UDID" grant "$SERVICE" "$BUNDLE_ID"
    ;;
  denied)
    xcrun simctl privacy "$UDID" revoke "$SERVICE" "$BUNDLE_ID"
    ;;
  *)
    printf 'Unknown permission state: %s
' "$STATE" >&2
    exit 64
    ;;
esac

実行が完了してからアプリを起動するか、テストを開始します。たとえば、連絡先が許可済みのシナリオでは、先にスクリプトを呼び出し、その後で対応するテストクラスだけを実行できます。

./set-permission.sh "$UDID" "$BUNDLE_ID" contacts authorized

xcodebuild test \
  -scheme PermissionTests \
  -destination "platform=iOS Simulator,id=$UDID" \
  -only-testing:PermissionUITests/ContactsAuthorizedTests \
  -resultBundlePath Artifacts/ContactsAuthorized.xcresult

resetは権限を未決定へ戻す操作であり、拒否を意味するものではありません。アプリケーションコードがこの2つの状態を同一に扱っている場合、テストでその設計上の問題を直接明らかにする必要があります。

システムダイアログとアプリの分岐を分けてテストする

権限ダイアログはシステムUIの一部であり、ボタンの特定は言語、ランタイムのバージョン、ダイアログの表示順序に左右されやすくなります。すべての権限テストをダイアログ操作に依存させてはいけません。より適切な階層分けは、大半のテストでgrantrevokeを使ってアプリ側の分岐を検証し、初回要求を確認するテストは少数だけ残す構成です。

初回要求のテストケース

最初にresetを実行し、アプリを起動して権限要求を発生させます。このテストでは、ダイアログが表示されることと、ユーザーが選択した後にアプリが想定どおりの画面へ進むことだけを確認します。システムUIを操作する必要がある場合は、独立したシステムアプリケーションオブジェクトを使用し、固定座標に依存しないようにしてください。

許可済みと拒否済みのテストケース

どちらのテストケースでも、権限ダイアログは表示されないはずです。許可済みの状態では、機能がデータを正常に読み取れることを検証します。拒否済みの状態では、アプリが権限要求を繰り返さず、無限の読み込み状態にも留まらないことを検証します。設定画面への案内がある場合は、ボタンと説明文だけを確認し、自動化ジョブ内で実際にシステム設定を変更してはいけません。

位置情報の権限では、アプリの使用中のみの許可や常時許可など、業務上の意味も区別する必要があります。simctl privacyで基本状態を準備することはできますが、実機上のすべての動作を代替できるわけではありません。そのため、関連する結論については実機での受け入れ確認も残しておく必要があります。

分離、証跡保存、失敗調査

各テストクラスの終了後に、対象サービスへresetを実行することはできます。ただし、それ以上に重要なのは、前のテストによるクリーンアップの成功に依存せず、次のテストが自ら必要な状態を作ることです。並列実行では、1台のシミュレータを1つのジョブだけに割り当てます。同時実行が必要なら、UDIDを共有せず複数のデバイスを作成してください。

失敗時には、少なくとも.xcresult、テストログ、シミュレータのシステムログ、現在のデバイス一覧を保存します。想定した状態が反映されていない場合は、次の順序で確認してください。

  1. Bundle Identifierが、実際にインストールされたアプリまたは拡張機能から取得されたものか。
  2. 現在のランタイムが、そのサービス名をサポートしているか。
  3. 状態を変更する前にアプリが終了されていたか。
  4. 対象UDIDが、xcodebuildで使用したデバイスと一致しているか。
  5. 別の並列ジョブが同じシミュレータを操作していないか。
  6. テストコードが古い権限判定結果をキャッシュしていないか。

「シミュレータ全体を消去する」ことを標準の修正方法にしてはいけません。ジョブの所要時間が大幅に増えるだけでなく、状態管理の不備を覆い隠す可能性もあります。説明できないシステム状態がデバイスで継続する場合に限り、それまでのログを保存したうえでデバイスを再作成してください。

MiniRentのクラウドMacでこの種のジョブを実行する場合、機種とノードが影響するのはジョブの配置方法だけです。テストスクリプトでは引き続き、UDID、Bundle Identifier、サービス名、目標状態によって環境を完全に記述し、現在選択できる構成をコンソールで確認してください。これにより、同じ権限回帰テストを一時的なデバッグジョブで再現できるだけでなく、継続的インテグレーションでも安定して実行できます。

よくある質問

simctl privacyだけで権限ダイアログのテストは完結しますか?

完結しません。権限状態の準備には有効ですが、実際のシステムダイアログの表示内容や操作経路は少数のUIテストで確認する必要があります。

権限テストのたびにシミュレータを消去する必要がありますか?

通常は不要です。アプリを終了し、対象のサービスとバンドルIDに対してreset、grant、revokeを実行すれば状態を分離できます。

独占利用できる物理デバイス

クラウドMac miniで次の開発・ビルドタスクを実行

2種類のM4構成、4つのレンタル期間、販売中の5つのノードから選択できます。実際の利用可能状況は、コンソールにリアルタイムで表示される情報をご確認ください。

構成を選んでレンタル