HireVM エンジニアリング記録

クラウドMacで再現可能なStoreKit課金回帰テストを構築する

クラウドMacで再現可能なStoreKit課金回帰テストを構築する

アプリ内課金モジュールで最も再現しにくい不具合は、たいてい「ボタンを押せない」ことではなく、テストケースをまたいで取引状態が漏れることです。最初のテストで購入を完了すると、次のテストでは未購入画面を検証するはずが、すでにアンロック済みの状態を読み取ってしまいます。クラウドMacで回帰テストを継続的に実行するには、シミュレータ、StoreKitセッション、アプリの永続化データをまとめて分離しなければなりません。そうしないと、単体では成功するのにテストスイート全体では失敗する状況が常態化します。

まずテスト境界を3層に分ける

第1層は純粋なビジネスステートマシンです。「未購入、処理中、購入済み、返金済み、期限切れ」を入力し、実際の取引を開始せずに機能の利用権限と画面状態を確認します。第2層ではStoreKit Testingを使用し、製品情報の取得、購入コールバック、取引リスナー、サブスクリプションの変更を検証します。第3層はリリース前に限定して実施する受け入れテストで、サーバー通知と本番の商品設定を確認するために使用します。

このように分割すれば、大半の回帰テストをローカルで完結でき、失敗の原因がビジネスロジック、クライアント側の取引アダプター、外部設定のどこにあるのかも明確になります。すべてのUIテストを購入ボタンのクリックから始めないでください。ステートマシンのユニットテストを中心とし、StoreKitの統合テストでは重要な経路だけを対象にします。

StoreKit Testingが提供するのは制御可能な取引環境であり、本番の取引経路を代替するものではありません。テストの成功は、指定したイベントに対してクライアントが正しく動作したことを示すだけで、外部設定の受け入れ確認まで完了したことを意味しません。

レビュー可能な商品カタログを作成する

プロジェクト内にStoreKit/Local.storekitを作成し、商品識別子をコードで使用する識別子と完全に一致させます。識別子がビューやテストに分散しないよう、一か所にまとめて定義することを推奨します。

enum ProductID {
    static let proMonthly = "com.example.app.pro.monthly"
    static let proYearly = "com.example.app.pro.yearly"
}

設定ファイルはバージョン管理に含めますが、テスト価格、表示名、サブスクリプション期間の変更についてもコードレビューを必須にします。チームでよく起きるミスは、古い設定をコピーして表示名だけを変更し、商品識別子を変更し忘れることです。その結果、商品照会で空の配列が返されます。

テスト専用のSchemeを作成し、たとえばStoreKitRegressionという名前を付けます。SchemeのRunとTestの両方で、同じ.storekitファイルを選択してください。開発者個人のSchemeに依存してはいけません。xcodebuildから認識できるのは共有Schemeです。

確認項目 期待される結果
商品識別子 設定ファイル、コード、アサーションが完全に一致している
Scheme Sharedに設定し、リポジトリへコミットしている
サブスクリプショングループ 同一グループ内のアップグレードとダウングレードの関係が明確である
ローカライズ テストのアサーションが変更されやすい表示文言に依存していない

各テストケースをクリーンなセッションから開始する

StoreKitTestでテストセッションを作成し、setUpでシステムダイアログを無効化して取引を消去します。テストメソッドの終了後には、アプリ自身が保存した利用権限のキャッシュも削除する必要があります。

import XCTest
import StoreKitTest

final class PurchaseRegressionTests: XCTestCase {
    private var session: SKTestSession!

    override func setUpWithError() throws {
        session = try SKTestSession(
            configurationFileNamed: "Local.storekit"
        )
        session.disableDialogs = true
        session.clearTransactions()
        UserDefaults.standard.removeObject(forKey: "cachedEntitlements")
    }

    func testMonthlyPurchaseUnlocksPro() async throws {
        try await session.buyProduct(
            identifier: ProductID.proMonthly
        )
        let unlocked = await EntitlementStore.shared.refresh()
        XCTAssertTrue(unlocked)
    }
}

取引リスナーは、購入を開始する前に必ず起動してください。購入が非常に速く完了すると、リスナーのタスクが更新を取りこぼし、テストが断続的にタイムアウトする可能性があります。返金、取り消し、サブスクリプションの期限切れについてもセッションから明示的に発生させ、利用権限を更新した後の結果を検証します。購入APIが成功を返したことだけを確認してはいけません。

固定待機で競合状態を隠さない

Task.sleepは失敗を先送りするだけで、状態が更新されたことを証明できません。より確実なのは、利用権限ストアから監視可能な状態を公開し、テスト側で明確な条件が成立するまで短いタイムアウト付きで待機する方法です。タイムアウト時のログには、少なくとも商品識別子、現在の利用権限、未完了の取引数、テスト名を記録します。ただし、トークンや完全な認証情報を出力してはいけません。

コマンドラインの実行経路を固定する

まずクラウドMacにインストールされているシミュレータデバイスを確認し、CIでは固定した名前を使用します。イメージ内のデバイス名が異なる場合は、スクリプトに任意のデバイスを暗黙的に選ばせるのではなく、パイプラインのパラメータを調整してください。

xcrun simctl list devices available
xcodebuild test \
  -workspace Example.xcworkspace \
  -scheme StoreKitRegression \
  -destination 'platform=iOS Simulator,name=iPhone 16 Pro' \
  -resultBundlePath Artifacts/StoreKitTests.xcresult \
  CODE_SIGNING_ALLOWED=NO

シミュレータテストだけを実行する場合は、コード署名を無効にすると取引ロジックと無関係な失敗を減らせます。結果ディレクトリは実行前に空にするか、ジョブ番号ごとに分けてください。既存のxcresultが残っていると、コマンドは直ちにエラーになります。

StoreKitの統合テストは、デフォルトでは直列実行のほうが安定します。並列実行が必要な場合は、実行単位ごとに独立したシミュレータ、Derived Data、結果ディレクトリを用意してください。複数のプロセスが同じシミュレータを同時に操作すると、取引キューとアプリデータが相互に汚染されます。

失敗を原因特定に役立つ証拠として設計する

少なくとも、初回購入、ユーザーによるキャンセル、保留、返金、サブスクリプションの更新、サブスクリプションの期限切れという6つの経路を対象にします。各経路では、StoreKitの返却状態、利用権限ストアの状態、最終的な画面状態という3つの層をすべて検証してください。ボタンの文言が変わったことだけを確認すると、バックグラウンドの利用権限が更新されていない問題を見落とします。

コミット前には、次の順序で確認できます。

  1. .storekitファイルと共有Schemeがバージョン管理に含まれている。
  2. 各テストケースの開始前に、取引とローカルの利用権限キャッシュを消去している。
  3. 取引リスナーが購入処理より先に起動している。
  4. テストが固定待機や実行順序に依存していない。
  5. 各並列ジョブが独立したシミュレータと成果物ディレクトリを使用している。
  6. 失敗時にxcresult、テストログ、画面の添付ファイルを保存している。
  7. 本番リリース前に、サーバー通知と商品設定を別途確認している。

HireVMでこの種の処理を実行する場合は、まずコンソールで現在選択可能な構成を確認し、テストシャード数に応じてシミュレータを計画してください。アプリ内課金テストは、1回のビルド速度よりも状態分離の影響を強く受ける傾向があります。まず1本の直列パイプラインが繰り返し成功する状態を作り、その後で並列度を上げれば、トラブルシューティングのコストを大幅に抑えられます。

よくある質問

StoreKit Testingだけで本番の課金確認をすべて代替できますか?

代替できません。商品ID、状態遷移、画面表示、エラー処理の確認には有効ですが、公開前にはサーバー通知、実際の商品設定、取引経路を別途確認する必要があります。

単独では成功するテストが一括実行で失敗するのはなぜですか?

前のテストによる取引、更新状態、アプリ内データの残留が主な原因です。各テストの開始前にセッションと保存状態を消去し、同じシミュレータの同時利用を避けます。

StoreKitテストは並列実行できますか?

各ワーカーに独立したシミュレータと結果ディレクトリを割り当てれば可能です。同じシミュレータやセッションを複数プロセスで共有する構成は避けてください。

専有Apple Silicon物理ノード

次のリモートMacタスクを専有ノードで実行

タスクの規模に合わせて、販売中の2種類の構成、5つのノード、日額・週額・月額・四半期の利用期間から選べます。注文前に構成と米ドル金額をすべて確認できます。

構成を選んで注文