アプリ内課金のステートマシンで最も再現が難しいのは、通常「購入成功」ではなく、キャンセル、保留、返金、そして前回の未完了トランザクションです。これらの分岐を外部の取引環境に直接依存させると、ネットワーク、商品設定、テストアカウントの状態が結果に入り込みます。より確実なのは、まずクラウド Mac 上で StoreKit 構成ファイルを使って商品を固定し、SKTestSession で取引条件を制御することで、クライアント側のロジックを繰り返し実行可能な回帰テストにする方法です。
オフラインテストの範囲を明確にする
StoreKit 構成は、商品 ID のマッピング、購入状態の遷移、エンタイトルメントの更新、エラー表示、トランザクション完了処理の検証に適しています。外部の決済経路へアクセスする必要がなく、失敗も再現しやすくなります。
ただし、本番の商品が正しく設定されていることは証明できず、サーバー通知、実際のレシート検証、最終的な支払い画面も対象外です。テストは次の 3 層に分けることを推奨します。
| レイヤー | 主な目的 | 実行頻度 |
|---|---|---|
| ユニットテスト | エンタイトルメントの算出、状態のマッピング | コミットごと |
| StoreKit 統合テスト | 購入、キャンセル、返金、未完了トランザクション | マージごと |
| 外部環境での受け入れテスト | 商品設定、通知、レシート処理経路 | リリース前 |
オフラインテストの価値はフィードバックループを短縮することにあり、あらゆる取引リスクを 1 つの緑色のチェックマークで覆い隠すことではありません。
商品とテストのエントリーポイントを固定する
Xcode で StoreKit Configuration File を作成し、テストに必要な商品だけを残します。商品 ID はコード内の定数と一致させてください。価格は UI テストにのみ使用し、ローカライズされた価格文字列を基にビジネスロジックがエンタイトルメントを判定しないようにします。
構成ファイルをテスト用 Scheme に追加し、StoreKitRegression.xctestplan などの専用テストプランを作成します。開発者が日常的に実行する Scheme は再利用しないでください。手作業による変更が CI の条件を気付かないうちに変えてしまう可能性があります。
テストターゲットでは、setUpWithError() 内でセッションを再作成できます。
import StoreKitTest
import XCTest
final class PurchaseRegressionTests: XCTestCase {
private var session: SKTestSession!
override func setUpWithError() throws {
session = try SKTestSession(configurationFileNamed: "Products")
session.disableDialogs = true
session.clearTransactions()
session.failTransactionsEnabled = false
}
override func tearDownWithError() throws {
session.clearTransactions()
session = nil
}
}
構成ファイル名には拡張子を含めません。初期化に失敗した場合は、まずファイルがテストターゲットに属していることを確認し、Scheme が同じ構成を使用しているかを確認してください。
取引の分岐を独立したテストケースに分ける
成功、返金、キャンセルを 1 つの長いテストケースで連続して検証しないでください。状態が増えるほど、失敗時にどの手順で汚染が生じたのか判断しにくくなります。少なくとも、次のケースに分割します。
通常購入と復元
購入直後にエンタイトルメントが更新され、ビジネスロジック層のオブジェクトを再作成した後も有効なエンタイトルメントを取得できることを検証します。テスト終了前に、アプリがトランザクションを処理して完了済みにしていることを確認し、次のテストケースで古い更新を再度受信しないようにします。
キャンセルと失敗の注入
キャンセル時に「支払い失敗」と表示したり、ロック解除済みの状態を書き込んだりしてはいけません。一方、ネットワークエラーや一般的な取引エラーでは、再試行の導線を残す必要があります。セッションプロパティでエラーを注入した後は、現在のテストが終了する前にデフォルト値へ戻し、次のテストケースによるクリーンアップに依存しないようにします。
返金と未完了トランザクション
返金後は対応するエンタイトルメントを取り消しますが、引き続き有効な別の商品まで誤って削除しないようにします。未完了トランザクションについては、ボタンのコールバックを一度確認するだけでなく、アプリの再起動後も処理を継続できることを検証してください。
同じ商品の状態に関係するテストは、直列実行を推奨します。並列テストでシミュレータと構成を共有すると、互いのトランザクションを消去し、CI でのみ発生する断続的な失敗を招きやすくなります。
コマンドラインで実行条件を固定する
最初に利用可能なシミュレータを確認し、デバイス名、OS バージョン、Xcode のパスを CI 変数に設定します。xcodebuild に任意の実行先を自動選択させないでください。
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
RESULT_DIR="$PWD/TestResults"
rm -rf "$RESULT_DIR"
mkdir -p "$RESULT_DIR"
xcodebuild test \
-workspace App.xcworkspace \
-scheme App-StoreKitTests \
-testPlan StoreKitRegression \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-resultBundlePath "$RESULT_DIR/StoreKit.xcresult"
OakVM 上で複数のパイプラインを同時に実行する場合は、ジョブごとに独立した作業ディレクトリとシミュレータを割り当て、DerivedData、結果ディレクトリ、起動済みデバイスを共有しないでください。開始前に xcrun simctl shutdown all を実行することもできますが、同じノード上の他のジョブにも影響するため、1 つのジョブがノードを専有する実行方式にのみ適しています。
証跡をアーカイブして失敗判定の基準を設ける
テスト失敗時にコンソール末尾の数十行だけを保存しても、通常は十分ではありません。xcresult、コミットハッシュ、StoreKit 構成ファイルのチェックサム、Xcode のバージョン、シミュレータランタイムのバージョンをアーカイブしてください。構成ファイルを変更した場合もコードレビューの対象とし、商品 ID が意図せず削除されるのを防ぎます。
まず、環境の概要を記録できます。
xcodebuild -version
xcrun simctl list runtimes
shasum -a 256 Tests/StoreKit/Products.storekit
チェックリストには、次の項目を含めます。
- 各テストが独自の
SKTestSessionを作成し、クリーンアップしているか - 自動ダイアログを無効にし、無人ジョブの停止を防いでいるか
- キャンセル、失敗、返金について、UI の状態とエンタイトルメントの状態をそれぞれアサートしているか
- テストが実行順序や前のトランザクションに依存していないか
- 失敗後も
xcresultがアップロードされるか - ログに機密性の高い認証情報や完全な取引データが含まれていないか
これらの条件を固定すれば、アプリ内課金の回帰テストは手作業のクリックに依存しなくなります。外部環境は本当に必要な検証だけを担当し、日々のコミットでは、高速で分離され、追跡可能なテストがクライアント側のステートマシンを守ります。
よくある質問
StoreKit のオフラインテストだけで実際の課金検証を完了できますか?
できません。クライアントの状態遷移や異常系には有効ですが、実際の商品設定、サーバー通知、レシート処理、決済画面は別のテスト環境で確認する必要があります。
単独では成功する課金テストが全体実行で不安定になるのはなぜですか?
前のテストからトランザクション状態が残っていないか確認します。テストごとに SKTestSession を作成して取引を消去し、同じ商品を変更するテストは直列実行します。
CI で課金テストが失敗した場合に保存すべき証跡は何ですか?
xcresult、テストログ、StoreKit 構成の版、コミットハッシュ、Xcode の版、シミュレータランタイムの版を一組で保存します。
次のビルドを専用物理ノードで実行
OakVM M4またはOakVM M4 Proを選択し、仮想マシンではないクラウド Macを日単位・週単位・月単位・四半期単位でレンタルできます。ノードの実際の利用可否はコンソールのリアルタイム情報をご確認ください。