The StoreKitTest framework drives in-app purchases from unit tests: buy, refund, expire, and ask-to-buy, all without the App Store. It pairs with StoreKit 2 and needs a StoreKit Configuration File in the project.
Session Setup
SKTestSession is the control surface. Clear stored transactions from earlier runs and turn off
the confirmation dialogs so purchases complete without UI.
@MainActor final class StoreKitTests: XCTestCase {
func testProductPurchase() async throws {
let session = try SKTestSession(configurationFileNamed: "SugarBot Food Calorie Counter")
session.disableDialogs = true
session.clearTransactions()
}
}
Simulating a Purchase
session.buyProduct(identifier:) simulates a purchase as if it happened outside the app. Then
load the product with StoreKit 2 and assert on how your store processes the subscription status.
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)
Expiry and Refunds
expireSubscription(productIdentifier:) ends an active subscription mid-test, so you can assert
the app drops entitlements. refundTransaction(identifier:) simulates a refund of a transaction
you bought earlier in the test.
try session.expireSubscription(productIdentifier: "annual")
let expiredStatus = try await Product.products(for: ["annual"])
.first?.subscription?.status ?? []
await store.processSubscriptionStatus(expiredStatus)
XCTAssertTrue(store.activeSubscriptions.isEmpty)
let transaction = try await session.buyProduct(identifier: "annual")
try session.refundTransaction(identifier: UInt(transaction.id))
Ask to Buy
Set askToBuyEnabled = true and the purchase lands as pending. Approve or decline it with
approveAskToBuyTransaction(identifier:) or declineAskToBuyTransaction(identifier:) and verify
the transaction moves from pending to purchased, or disappears.
session.askToBuyEnabled = true
await store.purchase("annual")
let pending = store.pendingTransactions.first?.id ?? 0
try session.approveAskToBuyTransaction(identifier: UInt(pending))