👨‍🔧

Xcode12から始めるStoreKit Testing

7 min read

概要

今までiOSにおけるアプリ内課金処理のユニットテストはとても書きづらく、もし書く場合は実際にApp Storeへの通信をしなければいけないと思います。
またアプリ内課金の実装を始めるまでに時間がかかったり(AppStore Connectでの様々な準備があるため)、サンドボックス環境での購入処理を実行してしまうと再度購入処理のテストを行うまでのインターバルが発生したり、アプリ内課金の実装のしづらさを感じていました。

しかしXcode12から導入された新機能によって様々な問題が解消されました。
今回はXcode12で導入された新機能と、アプリ内課金処理のユニットテストを書くためのStoreKit Testというフレームワークについて紹介していきます。

StoreKit Configuration

Xcode12からアプリ内購入に関することについて、Xcode内で様々な設定を行えるようになりました。
この機能を用いることでAppStoreのSandboxやProductionの環境にアクセスせずに、ローカル環境のみで動作確認やテストができるようになります。

StoreKit Configuration File

StoreKit Configuration Fileという、拡張子が.storeKitのファイルを生成できるようになりました。
このファイルに商品情報(プロダクトIDや価格 etc.)を設定でき、AppStoreConnectで色々な情報を登録せずに課金処理の動作確認をすることができます。

あくまでローカル環境での動作確認なので、SandboxやProductionの環境で動かすためにはAppStoreConnectでの情報の登録が必要です。

ファイルの作成の仕方と設定方法は下記のとおりです。

(1)ファイルの新規作成画面で、storekitでフィルタリングするとStoreKit Configuration Fileというファイルがあるのでそれを選択してください。

(2)作成したファイルを選択し、左下にあるボタンを押してください。

以下から実装したいIn-App Purchaseの種類を選択できます。

  • Add Consumable In-App Purchase:消耗型
  • Add Non-Consumable In-App Purchase:非消耗型
  • Add Auto-Renewable Subscription:定期購読型

今回はAdd Non-Consumable In-App Purchaseを選択します。

(3)以下の画像のように商品情報を記述します。

ここで設定した内容で購入処理ができるようになります。もちろん購入時のサインイン・認証は不要です。

(4)作成した設定ファイルを利用する設定を行います。

Edit Schemeから、StoreKit Configurationという項目に作成したStoreKit Configuration Fileを選択します。

これで設定は完了しました!後はStoreKitを用いてアプリ内課金の実装をすればすぐに動作確認ができます。
githubに動作できるコードを置いてあるので、良ければ確認してみてください。

StoreKit Transaction Manager

StoreKit Transaction Managerとは、ローカルのテスト環境での過去の購入履歴(厳密には購入のトランザクション)を見ることができたり、それらを制御できたりする新機能です。

例えばローカル環境で一度購入処理を行った後に、リファクタリングやバグ修正などのために、購入処理を再実行したいことがあると思います。
このときStoreKit Transaction Managerから購入履歴を削除し、未購入の状態に戻すことができます。

まずXcode下部にある、ボタンをクリックします。

するとTransaction Managerの面画が開きます。トランザクションを削除をすることで、未購入の状態に戻すことができ、また返金済みの状態に変更することができます。

また.storekitのファイルを開いた状態でXcodeのEditタブを選択すると、Enable Interupted Purchasesで購入の失敗や、Ask To Buyの有効、Time Rateでサブスクリプションの期限を変更するなど様々な状態を作ることができます。

このようにして購入トランザクションの状態変更やエラーを意図的に発生させることができるので、開発の効率が格段に上がりました。

StoreKit Testing

StoreKit Configuration Fileを使用することでユニットテストも書きやすくなりました。
In-App Purchaseに関するユニットテストを書くには、StoreKit TestというiOS14から利用できるフレームワークを使用します。

SKTestSession

コード上でIn-App Purchaseの処理を実行した際、トランザクションを処理するときに使用する設定を制御するクラスです。
SKTestSessionの特定のインスタンスメソッドを呼び出すたびにSKTestTransactionが生成されます。
また上記に加えて、SKPaymentTransactionObserver経由で実際のトランザクション(SKPaymentTransaction)も生成されているようです。

StoreKitのテスト環境は一つしかなく、すべてのSKTestSessionはその一つしかないテスト環境を制御します。つまりSKTestSessionのインスタンスを複数生成し、並行処理でそれぞれのインスタンスのAPIから制御を行おうとしても、それぞれの処理に影響を与えてしまいます。詳しい事例は後述します。

以下はIn-App Purchaseのユニットテストを書く際の必要最低限な設定です。

import XCTest
import StoreKitTest

private var session: SKTestSession!

override func setUpWithError() throws {
    // 接続先をローカル環境に設定し、Configuration.storekitで設定した内容にアクセスするようにします.
    session = try SKTestSession(configurationFileNamed: "Configuration")
    // テスト中はユーザーからの操作を無くすため、すべてのダイアログを無効にします.
    session.disableDialogs = true
    // 過去のトランザクションをすべて削除します.
    session.clearTransactions()
}

最後に特定の異常系のテストを書く際に必要なAPIをいくつか紹介します。

Failed Transactions

トランザクションを強制的に失敗させたい場合は、failTransactionsEnabledというプロパティにtrueをセットします。

func testPurchaseBerryBlue_failed() {
    guard let product = product else {
        XCTFail("Must fetch product")
	return
    }
    // trueにすることで、トランザクションが必ず失敗するようにしている.	
    session.failTransactionsEnabled = true
    purchase(product: product)
    
    wait(for: [purchaseProductExp], timeout: 3.0)
    XCTAssertTrue(transactionState == .failed, "\(transactionState.rawValue) is not failed")
}

あるテストケースでfailTransactionsEnabledをtrueに設定後、別のテストケースでトランザクションを開始するとそれも失敗してしまいます。前述したようにテスト環境は一つのみであり、SKTestSessionはその一つしかない環境を制御します。そのため一度failTransactionsEnabledをtrueにしてしまうと、それ以降も常にトランザクションが失敗する状態になってしまうようです。デフォルトを成功状態にしておきたい場合は、setUpで明示的にsession.failTransactionsEnabled = falseとしておくと良いと思います。
後述するinterruptedPurchasesEnabledをtrueにした場合も同様です。

Interrupted Purchases

購入処理が中断された場合のテストを行いたいなら、interruptedPurchasesEnabledというプロパティをtrueにセットします。
そしてユーザーが必要な手続きを完了した場合のテストを行いたいなら、その後resolveIssueForTransactionというメソッドを実行します。

func testPurchaseBerryBlue_successAfterInterrupted() {
    guard let product = product else {
        XCTFail("Must fetch product")
	return
    }
    // トランザクションが必ず中断されるようにしている.
    session.interruptedPurchasesEnabled = true
    purchase(product: product)
    
    wait(for: [purchaseProductExp], timeout: 3.0)
    session.allTransactions().forEach { transaction in
        do {
	    // トランザクションが中断された原因を、ユーザーが解決したことをシミュレートするようにしています.
	    try session.resolveIssueForTransaction(identifier: transaction.identifier)
	} catch {
	    XCTFail()
	}
    }
    
    purchaseProductExp = XCTestExpectation(description: "Purchase product item")
    // SKPaymentTransactionObserverでトランザクションの状態が通知されるのを待ちます.
    wait(for: [purchaseProductExp], timeout: 3.0)

    XCTAssertTrue(transactionState == .purchased, "\(transactionState.rawValue) is not purchased")
    SKPaymentQueue.default().finishTransaction(transaction!)
}

その他にも様々な状況をシミュレートするためのAPIが提供されているので、SKTestSessionのリファレンスから、書きたいテストケースに応じたAPIを探してみてください。

正常系と1種類の異常系のテストケースのみですが、こちらもgithubにコードを上げているので、もし良ければ確認してみてください。

まとめ

Xcode12やiOS14からIn-App Purchaseの実装がとてもやりやすくなったと思います。
個人的にはサブスクリプションの動作確認の待ち時間を無くせるようになったことがとても嬉しかったです。またなにか間違っていることがあればコメントいただけると嬉しいです。

参考URL

Discussion

ログインするとコメントできます