Preface

The second iteration of the StoreKit framework is the most significant change I’ve seen in applications over the past few years. Recent versions of the StoreKit framework have fully adopted Swift language features such as async and await. In this article we will discuss the StoreKitTest framework, which is not part of StoreKit 2 but is tightly coupled to it.

StoreKitTestThe framework provides us with SKTestSessiontypes. Using SKTestSessioninstances of type, we can purchase in-app products, manage transactions, refunds and expired subscriptions, etc.

Create a StoreKit Demo

We StoreKitstart by creating a test case for the relevant functionality. I usually have a SettingsStoretype called which defines the user configuration and handles in-app purchases. We’ll use the framework to cover the in-app purchase management part of StoreKitTestthrough testing .SettingsStore

Copy code
@MainActor final class StoreKitTests: XCTestCase {
    func testProductPurchase() async throws {
        let session = try SKTestSession(configurationFileNamed: "SugarBot Food Calorie Counter")
        session.disableDialogs = true
        session.clearTransactions()
    }
}

As shown in the above example, we initialize SKTestSessionan instance of type. We then call clearTransactionsthe function to delete any transactions we may have stored from previous launches. We also turn off dialog boxes to easily automate the purchase confirmation process.

Using SKTestSession

Now we can use our SettingsStoretype to purchase the product and handle subscription status. SKTestSessionThe type also allows us to purchase a product that simulates an out-of-app purchase. For example, it might be a purchased product that has Home Sharing enabled.

Copy code
@MainActor final class StoreKitTests: XCTestCase {
    var store: SettingsStore!
    
    override func setUp() {
        store = SettingsStore()
    }
    
    func testProductPurchase() async throws {
        let session = try SKTestSession(configurationFileNamed: "SugarBot Food Calorie Counter")
        session.disableDialogs = true
        session.clearTransactions()
        
        try await session.buyProduct(identifier: "annual")
        
        guard let product = try await Product.products(for: ["annual"]).first else {
            return XCTFail("Can't load products...")
        }
        
        let status = try await product.subscription?.status ?? []
        await store.processSubscriptionStatus(status)
        
        XCTAssertFalse(store.activeSubscriptions.isEmpty)
    }
}

As shown in the example above, we use a function SKTestSessionof type buyProductto simulate a purchase. We can also use functions SKTestSessionof type expireSubscriptionto expire ongoing subscriptions and verify how our application handles this data.

Copy code
@MainActor final class StoreKitTests: XCTestCase {
    var store: SettingsStore!
    
    override func setUp() {
        store = SettingsStore()
    }
    
    func testExpiredProduct() async throws {
        let session = try SKTestSession(configurationFileNamed: "SugarBot Food Calorie Counter")
        session.disableDialogs = true
        session.clearTransactions()
        
        let transaction = try await session.buyProduct(identifier: "annual")
        
        let activeProducts = try await Product.products(for: ["annual"])
        let activeStatus = try await activeProducts.first?.subscription?.status ?? []
        await store.processSubscriptionStatus(activeStatus)
        
        XCTAssertFalse(store.activeSubscriptions.isEmpty)
        
        try session.expireSubscription(productIdentifier: "annual")
        
        let expiredProducts = try await Product.products(for: ["annual"])
        let expiredStatus = try await expiredProducts.first?.subscription?.status ?? []
        await store.processSubscriptionStatus(expiredStatus)
        
        XCTAssertTrue(store.activeSubscriptions.isEmpty)
    }
}

SKTestSessionThe type also allows us refundTransactionto simulate product refunds using the function. Another exciting option is to test how the application reacts to transaction updates.

Copy code
let transaction = try await session.buyProduct(identifier: "annual")
// 
try session.refundTransaction(identifier: UInt(transaction.id))
// 

askToBuyEnabled Property

You can also use askToBuyEnabledthe attribute to enable the Ask to Buy feature, and then use the approveAskToBuyTransactionor declineAskToBuyTransactionfunction to approve or deny the purchase. In this case, the transaction should change from pending to successful.

Copy code
session.askToBuyEnabled = true

await store.purchase("annual")
// 

let declined = store.pendingTrancations.first?.id ?? 0
try session.declineAskToBuyTransaction(identifier: UInt(declined.id))
// 

await store.purchase("annual")
// 

let approved = store.pendingTrancations.first?.id ?? 0
try session.approveAskToBuyTransaction(identifier: UInt(approved.id))
// 

As shown in the example above, we use SKTestSessioninstances of type to simulate asking for a purchase and verify how our application behaves when a purchase is approved or denied.

Summarize

This article describes how to create test cases, then details how to use the SKTestSession type to simulate purchases, refunds, and subscription expirations, and shows how to test the application’s handling of these situations. It also describes how to enable the ask-to-buy feature using the askToBuyEnabled attribute, and shows how to verify the application’s behavior when a purchase is approved or denied. Through this article, readers can learn how to use the StoreKitTest framework to verify the application’s ability to handle in-app purchases and user flows.

Leave a Reply

Your email address will not be published. Required fields are marked *