Skip to content

StoreKit Testing (SwiftUI)

A SwiftUI guide for coding agents. Also covers sktestsession, test in app purchases, simulate subscription expiry, refund transaction test, ask to buy testing, storekit configuration file.

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))

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-storekit-testing-in-swift" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems