エンジニアリング実践

クラウド Mac で iOS プッシュ通知を simctl により自動検証する

クラウド Mac で iOS プッシュ通知を simctl により自動検証する

リモートパイプラインでインストール可能な iOS アプリを生成できても、チームが常時接続されたテスト端末を確保できるとは限りません。この状況で見落としやすいのはコンパイルエラーではなく、通知権限、フォアグラウンド表示、ディープリンクのパラメータ、通知サービス拡張の組み合わせに起因する問題です。simctl push を使えば、APNs 形式のペイロードを iOS Simulator に直接注入できるため、クラウド Mac 上で迅速かつ再現可能なコミット前検証を実施できます。

実際の APNs 配信テストを置き換えることはできませんが、クライアント側の多くの問題をビルド段階で早期に検出できます。重要なのは、コマンドの終了コードだけで判断せず、アプリの状態、スクリーンショット、統合ログも併せて保存することです。

このテストで何を証明できるかを先に定義する

simctl push が検証するのは、ビルド済みアプリが通知ペイロードをどのように処理するかです。通知内容の解析、フォアグラウンドおよびバックグラウンドでのデリゲートコールバック、カテゴリボタン、ディープリンクのルーティング、Notification Service Extension の処理結果を確認できます。

一方、サーバー側の認証、デバイストークンの有効性、外部ネットワーク経由の配信、APNs のレート制限、実機の省電力動作は検証できません。Simulator がペイロードを受信しても、本番環境のプッシュ通知経路が開通していることにはなりません。

コマンドの成功は「ローカルへの注入が完了した」ことを意味するだけで、「ユーザーが通知を受信した」ことを意味しません。検証結果は、画面表示、アプリログ、ルーティング結果に基づいて判定する必要があります。

開始前に、アプリで通知機能が宣言され、テストフロー内で権限が付与されていることを確認します。simctl push がアプリに代わってシステムの権限ダイアログを操作することはありません。初回の権限付与は UI テストで処理するか、専用のテスト用 Simulator で事前に済ませられますが、長期間使い続けている特定の Simulator の状態に依存してはいけません。

Simulator とアプリの入力を固定する

並列ジョブでは booted を直接使用しないでください。複数の Simulator が同時に起動していると、別のジョブへペイロードが送られる可能性があります。ジョブごとに明示的な UDID を記録し、アプリのパス、Bundle ID、成果物ディレクトリを入力として指定します。

set -euo pipefail

: "${SIMULATOR_UDID:?Set SIMULATOR_UDID}"
: "${APP_PATH:?Set APP_PATH}"
: "${BUNDLE_ID:?Set BUNDLE_ID}"

ARTIFACTS="${ARTIFACTS:-$PWD/.ci/push-check}"
mkdir -p "$ARTIFACTS"

xcrun simctl bootstatus "$SIMULATOR_UDID" -b
xcrun simctl install "$SIMULATOR_UDID" "$APP_PATH"
xcrun simctl launch "$SIMULATOR_UDID" "$BUNDLE_ID"
xcrun simctl get_app_container "$SIMULATOR_UDID" "$BUNDLE_ID"

bootstatus -b はシステムの起動が実際に完了するまで待機するため、起動後に一定秒数スリープする方法より安定します。get_app_container は低コストなインストール確認にもなります。Bundle ID が誤っている場合やアプリがインストールされていない場合は、通知を送信する前にジョブが失敗します。

初回の権限付与を再検証する場合は、共有デバイスですべてのプライバシー設定を安易にリセットせず、そのジョブ専用の Simulator を新規作成するか消去してください。これにより、並列テストがカメラ、写真、通知などの権限を相互に変更する事態を防げます。

APNs ペイロードを生成して検証する

ペイロードファイルには JSON を使用します。スクリーンショット、アプリログ、パイプラインジョブを対応付けられるよう、検索可能なテスト識別子を追加することを推奨します。

PAYLOAD="$ARTIFACTS/foreground.apns"
TRACE_ID="push-check-${CI_RUN_ID:-local}"

cat > "$PAYLOAD" <<JSON
{
  "Simulator Target Bundle": "$BUNDLE_ID",
  "aps": {
    "alert": {
      "title": "Build completed",
      "body": "Open the result for details"
    },
    "sound": "default",
    "badge": 1,
    "category": "BUILD_RESULT"
  },
  "route": "build/result",
  "trace_id": "$TRACE_ID"
}
JSON

plutil -lint "$PAYLOAD"
PAYLOAD_BYTES="$(wc -c < "$PAYLOAD" | tr -d ' ')"
test "$PAYLOAD_BYTES" -le 4096
xcrun simctl push "$SIMULATOR_UDID" "$BUNDLE_ID" "$PAYLOAD"

4KB のチェックは異常なペイロードを早期に検出するためのものですが、Simulator がサーバー側のすべての検証を完全に再現するわけではありません。アプリ側でも route、識別子、カスタムフィールドの型を検証し、テスト用ペイロードを信頼できるという理由で強制キャストしてはいけません。

通知サービス拡張を使用する場合は "mutable-content": 1 を追加し、拡張から共通形式の構造化ログを出力します。拡張の処理時間やシステムによる強制終了についても失敗経路を前提に設計し、ダウンロードや内容の書き換えが必ず完了するとは想定しないでください。

アプリの状態別に検証マトリクスを作成する

同じペイロードについて、少なくともフォアグラウンド、バックグラウンド、未起動の 3 状態を検証する必要があります。フォアグラウンド通知でバナーを表示するかどうかは、通常デリゲートメソッドによって決まります。そのため、「バナーが表示されない」という結果は製品仕様の場合もあれば、コールバックの実装漏れの場合もあります。状態ごとの期待結果を明記してください。

シナリオ 操作 必須確認項目
フォアグラウンド アプリを起動してから注入 デリゲートコールバック、アプリ内表示、イベントの重複
バックグラウンド ホーム画面に戻ってから注入 バナーの内容、バッジ、タップ時のルーティング
未起動 アプリを終了してから注入 コールドスタート時の入口、パラメータ解析、遷移先画面
コンテンツ拡張 mutable-content を含むペイロードを注入 拡張のログ、内容の書き換え、タイムアウト時のフォールバック
不正なフィールド ルートの欠落またはフィールド型の誤り 安全なフォールバック、クラッシュしないこと、診断可能なログ

送信後はスクリーンショットを保存し、パイプラインに「コマンド成功」という実質的な証拠のない結果だけが残らないようにします。

xcrun simctl io "$SIMULATOR_UDID" screenshot \
  "$ARTIFACTS/notification.png"

xcrun simctl spawn "$SIMULATOR_UDID" log show \
  --last 2m \
  --style compact \
  --predicate "process == '$BUNDLE_ID'" \
  > "$ARTIFACTS/application.log"

プロセス名が Bundle ID と一致するとは限りません。より確実なのは、アプリと拡張で固定のログ subsystem を使用し、subsystem を条件に検索する方法です。ログにはテスト識別子、状態、エラー種別のみを記録し、通知本文の全文、トークン、ユーザーデータは出力しないでください。

失敗条件をパイプラインに組み込む

信頼できる通知検証ジョブでは、shell コマンドを実行するだけでなく、成果物の有無、アプリのクラッシュ、想定したルートの記録も判定する必要があります。テスト用ビルドでは、処理済みの trace_id と結果を専用ファイルへ書き出し、simctl get_app_container でコンテナを特定して読み取る方法を利用できます。このテスト用インターフェースは内部ビルド構成でのみ有効にし、リリース構成では必ず無効にしてください。

よくある誤判定には、権限付与済みの Simulator を再利用して初回ダイアログの検証を漏らす、booted の使用によって並列ジョブが混線する、フォアグラウンドでバナーが表示されないことを注入失敗と判断する、スクリーンショットだけを確認してタップ後のルーティングを検証しない、ログの検索範囲が広すぎて前回のジョブの記録を読み込む、といったものがあります。

コミット前には、次の順序で最終確認を行います。

  1. Xcode、ランタイム、デバイスモデル、Simulator の UDID を固定する。
  2. 今回のビルド成果物をインストールし、古いアプリコンテナを再利用しない。
  3. JSON の構文、バイト数、必須フィールドを検証する。
  4. フォアグラウンド、バックグラウンド、未起動、不正なペイロードの各テストケースを個別に実行する。
  5. ペイロード、スクリーンショット、アプリログ、一意のテスト識別子をアーカイブする。
  6. Simulator での検証と、実際の APNs を使ったエンドツーエンドテストを分けて報告する。

以上の手順を整備すれば、プッシュ通知のクライアントロジックは、人による目視確認に依存する機能から、繰り返し実行でき、証拠を保存し、失敗した段階を正確に特定できるエンジニアリング上の検査へと変わります。

よくある質問

simctl push の成功は実際の APNs 配信成功を意味しますか?

いいえ。確認できるのは Simulator へのローカル注入だけです。サーバー認証、デバイストークン、ネットワーク配信、APNs の制限は管理された実機で別途確認します。

コマンドが成功してもバナーが表示されないのはなぜですか?

通知権限、aps の内容、アプリの状態を確認します。アプリが前景にある場合は、通知デリゲートでバナー表示を明示的に許可する必要があります。

CI では booted と Simulator の UDID のどちらを使うべきですか?

ジョブに割り当てた UDID を使います。並列実行中に booted を使うと、別の Simulator に通知やスクリーンショットが送られる可能性があります。

専用物理 Mac mini

次の開発パイプラインにクラウド Macを選ぶ

3種類の固定構成とアジアの4ノードを比較し、実際のワークロードに合ったレンタル期間を選択できます。レンタル1件につき専用物理マシン1台が割り当てられ、仮想マシンではありません。

構成を選んでレンタル