Xcode: SKTestSessionを使う
XcodeでStoreKit/StoreKit2のUnitTestを自動実行するには、SKTestSession
というクラスを使います。
その覚え書き。
-
SKTestSession(configurationFileNamed: ".storekit設定ファイル名(拡張子を除く)")
で「ファイルが見つかりません」エラーが出て進めない場合があります。解消するには、.storekitファイルをUnitTestターゲットの対象に含めます。
具体的には、Xcode上で.storekitファイルを選んだ状態で右サイドパネルを開き、Target MembershipのところでUnitTestターゲットのチェックボックスにチェックを入れます。
SKTestSession
の.locale
および.storefront
はテストコード内からコードで変更できます。
session.locale = Locale(identifier: "ja_JP")
session.storefront = "JPN"
ちなみに、app.launch()
の後に設定しても変わらないようです。app.launch()
の前で設定します。
あと、.storefront
の文字列がどういう体系なのかまだ調べていません。
日本は"JPN"
、米国は"USA"
のようです。
StoreKitのProduct.displayPrice
は、iOS 16では"$100.00"
や"¥100"
を返しますが、もしかするとiOS 17では"US$100.00"
、"JP¥100"
を返すようになるのかも知れません。
SKTestSession.clearTransactions()
がSimulatorのトランザクションをクリアしてくれません。
具体的には、setUpWithError()
内で.clearTransactions()
を呼ぶのだけども、Simulatorに購買履歴が残ったままの様子で、初回は未購入の状態からテストできるのに、2回目以降は購入済みになっています。
もう少し詳しく言うと、以下のようになります。
- デバッグ実行するときのSimulatorとテスト実行するときのSimulatorが違っている(インスタンス?というのかな)。
- テスト実行のほうのSimulatorには「Clone 1 of iPhone…」というような名前がついている。デバッグ実行のほうは「Clone 1」とかはなし。
- デバッグ実行のほうにはトランザクションが残ってないのだけども、Clone 1のほうには残ってしまっているようで、Clone 1で普通にアイコンタップしてAppを起動すると、購入済み状態になってしまっている。デバッグ実行のほうは未購入に戻っている。
-
.clearTransactions()
がデバッグ実行のほうのSimulatorをクリアしているのかというとそうではなくて、単にデバッグ実行の方では購入してないだけ。デバッグ実行のほうで購入すれば購入済みになる。こっちはXcodeのDebugメニュー→StoreKit→Manage Transactions…でトランザクションの編集(削除も)ができる。Clone 1のほうはXcodeからは編集できない。
待機時間を入れたり、.allTransactions()
→.deleteTransaction()
したり、順序を入れ替えたり、と色々試した結果、テスト終了時に.clearTransactions()
を呼ぶとうまくクリアされるように見えます。
tearDownWithError()
に入れるか、個々のテストケースに突っ込むか、考えどころです。
タイミングに依存していると思われるので、他の回避方法もあるような気がします。
Xcode 14.3.1 + iOS 16.4です。
SKTestSession
を使ったテストで、購入処理のあとのダイアログを表示しなくするには、.disableDialogs = true
を設定するのですが、じゃあ表示させてそのダイアログを含めたテストをできないのか?という話です。
このダイアログ、Appから表示されていなくて、Appの上に重ねて表示されているようで、このダイアログが表示された状態でAppを終了してもダイアログだけ残ります。他のAppなのかOSなのか、とにかくAppのプロセスとは切り離されています。
このため、このダイアログで「購入」あるいは「×」ボタンを押す動作のシミュレートはXCUITestではできなさそうです。(.launch()
したXCUIApplication
からアクセスする方法がない)
ちなみに、この「×」ボタンを押すとダイアログが閉じて元の画面(通常、自分で作った「購入」ボタンがある画面)に戻るのですが、この閉じて戻る(そしてトランザクションが発生しない)という動きは、ダイアログが表示された状態でホームボタンを押しても発生します。
これを利用すると、このダイアログのボタンが押された後の動作のテスト、というようなこともできそうです。
- 「購入」ボタンのシミュレーション:あらかじめ
.disableDialogs = true
にしてダイアログを出さない - 「×」ボタンのシミュレーション:自分の購入ボタンを押した後、
XCUIDevice.shared.press(XCUIDevice.Button.home)
を呼ぶ
にボタンを押す方法を書きました。
ペアレンタルコントロールで親の承認が必要な場合、Pending Approvalという状態になるようですが、この状態のとき、自前の購入ボタンを再度押せないようにしたほうが良いのか?という疑問があります。
回答が以下にありました。
There is no need to manage in your UI pending In-app purchases for the 24-hour period. While it's recommended at time of purchases, once the customer navigates away and back to the In-app purchase buy button, they are safe to re-request if they so wish. Any duplicate Ask to Buy requests are consolidated and only resets the 24-hour timer. Once approved, only 1 purchase is completed.
ちょっと分かりにくい説明のような気がするのですが、意味合いとしては以下のような感じではないかと思います。
- 自前の購入ボタンを押して、「Ask Permission」ダイアログが表示され、そこで「Ask」を選び、画面に戻った、というまさにその時点では、自前の購入ボタンは押せなくなっていることが望ましい。
- ただし、その後にいったん他の画面にいったりAppから離れたりした後で戻ってきたときには、自前の購入ボタンは押せるようになっていても構わない。仮に押されたとしても、二重購入にはならない。
- 重複した「Ask to Buy」要求は1つにまとめられる。ただし要求される都度、24時間タイマーがリセットされる。
- 承認された場合は、1回の購入だけが完了する。
24時間タイマー、というのは以下のページで触れられている機能(太字にしました)のような気がします。
管理者がリクエストを却下した場合は、お子様にリクエストが却下されたことを知らせる通知が届きます。管理者がリクエストを見落とした場合や、購入手続きをしなかった場合、お子様はもう一度リクエストを送信する必要があります。iOS 16.1 または iPadOS 16.1 以前では、リクエストを拒否した場合やそのまま閉じた場合、そのリクエストは 24 時間後に削除されます。
これ16.2以降はどうなったのか気になりますが。
削除されないのかな?
ちなみに、「画面に戻った、というまさにその時点」というのはProduct.purchase()
を呼んでProduct.PurchaseResult
が返ってきているタイミングです。
Pending Approvalの場合、PurchaseResult = .pending
になります。なのでこれを見て購入ボタンを押せないようにすれば良いのだろうと思います。ただそれ以外にも.pending
になるケースがあるようです。とは言え、まだどういうケースなのか調べ切れていません。
ただそれ以外にも
.pending
になるケースがあるようです。
以下に記載のある、interrupted purchaseというのが該当するようです。
In general, an interrupted purchase is experienced anytime a customer needs to address an issue with their Apple ID. For example, they may need to agree to updated terms and conditions or update an expired payment method.
AppleIDの問題、利用規約の変更への同意がまだ、決済手段の期限切れへの対応(更新)、などの際に発生するということですね。
以下にStoreKit1時代の記載がありました。
StoreKit2+SKTestSession
利用前提で、「Begin testing」部分を書くとすると以下のようになるでしょうか。
【事前準備】
- Xcodeで.storekitファイルを選び、Editorメニュー→Enable Interrupted Purchasesを選択(メニュー項目がDisable Interrupted Purchasesになります)
【テスト】
- ユーザは、アプリを使って自前の「購入」ボタンを押して、アプリ内購入を行います。アプリは
Product.purchase()
を呼びます。 - システムにより、支払いシートが表示されます。ユーザは「購入」ボタンを押します。
SKTestSession
では.disableDialogs = true
にすると同等の動作になります。 -
Product.purchase()
が、PurchaseResult.pending
を返してくることを確認します。StoreKit.Transaction
は取得できません。(なのでTransaction.finish()
も呼べません) - XcodeのDebugメニュー→StoreKit→Manage Transactions...で、トランザクションがFailedになっていることを確認します。
- XcodeでFailedのトランザクションを選択し、Resolve Issuesボタンを押して利用規約に同意したことをシミュレートします。
SKTestSession
では.resolveIssueForTransaction(identifier:)
を呼ぶと同等の動作になります。 -
Transaction.updates
に新しいトランザクションが生成されて、VerifiedResult<Transaction>
が.verified
になっていることを確認します。 - アプリでの購入処理(有効化処理)が実行され、サービスまたは製品(機能)が提供されていることを確認します。
- アプリが、生成されたトランザクションを
.finish()
することを確認します。
こんな感じでしょうか。
XCUITestするにはFailedのトランザクションのIDが必要ですが、どう取得するのがいいのか、調べましょうか。
Ask to Buyは、もとのトランザクションが完了されないまま残り(24時間経つと消える)、承認されるとそれが更新されるようなイメージなのに対し、interrupted purchaseの場合は最初のトランザクションがFailedのまま完了し(StoreKit1では自分で完了させ)、新たなトランザクションが生まれるようです。
XCUITestするにはFailedのトランザクションのIDが必要ですが、どう取得するのがいいのか、調べましょうか。
このトランザクションはStoreKit.Transaction
ではなくて、SKTestTransaction
でした。
テストなのでSKTestSession
の.allTransactions().last
で取得すれば充分ではないかと思います。
StoreKit2の場合、「Ask to Buy」で承認された場合は、Transaction.updates
に更新されたトランザクションが入ってきます。一方、却下された場合は(おそらく24時間タイムアウト時も)何も起きません。
.pending
になったときに購入ボタンを押せなくする場合、却下後に元に戻す(購入ボタンを再度押せるようにする)きっかけがありません。
どうするのがいいのでしょうか。
.pending
になったときに購入ボタンを押せなくする場合、却下後に元に戻す(購入ボタンを再度押せるようにする)きっかけがありません。
interrupted purchaseの場合も同様で、再度押せるようにするきっかけがありません。
自前の購入ボタンを押して、「Ask Permission」ダイアログが表示され、そこで「Ask」を選び、画面に戻った、というまさにその時点では、自前の購入ボタンは押せなくなっていることが望ましい。
ということだったのですが、StoreKit2の処理の流れ的に、たぶんこれは無理ですね。購入したときの通知はあるものの、購入しないときには何もトリガーがありません。なので、押せなくしたときに押せるように戻すのがタイマーぐらいしかなくなります。そのタイマーも妥当性のある間隔というのが存在しない。
「押せてしまっても良い」ということだったのでそうします。
ふと思いましたが、interrupted purchaseのケースで、ずっとクレジットカードの期限が切れたまま放置していて、購入したことを忘れていたようなパターンがあったとして、何かのきっかけでクレジットカードの登録内容を更新したら、ずいぶん前に購入ボタンを押したものがピロッと支払われてしまう、というケースもありそうです。
こっちにもタイムアウトあるのでしょうか。
に、StoreKitでのエラーへの対処方法案が書かれています。
ただ、StoreKit2になって、このうちの多くのエラーケースがProduct.PurchaseResult = .success
にならずに、.pending
になってしまって、エラー自体が取れずApp側で原因を分類できない(メッセージの表示分けとかができない)ような気がしています。
.success
にならないとVerificationResult
も取れず、そこからのエラーも取れません。
Transaction.unfinished
とか.all
で、未完了のトランザクションを取って確認するべきかと思いきや、.all
にもトランザクションがいないという状態です。
普通、どうするものなのでしょうか。