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.
StoreKitTest
The framework provides us with SKTestSession
types. Using SKTestSession
instances of type, we can purchase in-app products, manage transactions, refunds and expired subscriptions, etc.
Create a StoreKit Demo
We StoreKit
start by creating a test case for the relevant functionality. I usually have a SettingsStore
type 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 StoreKitTest
through 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 SKTestSession
an instance of type. We then call clearTransactions
the 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 SettingsStore
type to purchase the product and handle subscription status. SKTestSession
The 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 SKTestSession
of type buyProduct
to simulate a purchase. We can also use functions SKTestSession
of type expireSubscription
to 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)
}
}
SKTestSession
The type also allows us refundTransaction
to 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 askToBuyEnabled
the attribute to enable the Ask to Buy feature, and then use the approveAskToBuyTransaction
or declineAskToBuyTransaction
function 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 SKTestSession
instances 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.