StoreKit Testing in Xcode 12: Getting Started

Learn to use the new StoreKit testing framework to exercise your in-app purchases right inside Xcode. By Saleh Albuga.

4.6 (9) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

Starting Automated Testing

Open GreenBarTests.swift and add the following line below import XCTest:

import StoreKitTest
@testable import GreenBar

This code imports the StoreKitTest framework and your app so you can run tests against it.

Next, add the following test method inside GreenBarTests:

func testReceiptValidation() throws {
  // 1
  let session = try SKTestSession(configurationFileNamed: "RecipesAndCoins")
  session.resetToDefaultState()
  session.disableDialogs = true

  // 2
  let identifier = GreenBarContent.tartCherrySalad
  let expectation = XCTestExpectation(description: "Buy Content")

  // 3
  GreenBarContent.store.requestProductsAndBuy(
    productIdentifier: identifier
  ) { _ in
    // 4
    let receipt = Receipt()
    if let receiptStatus = receipt.receiptStatus {
      let validationSucceeded = receiptStatus == .validationSuccess
      XCTAssertTrue(validationSucceeded)
    } else {
      XCTFail("Receipt Validation failed")
    }

    expectation.fulfill()
  }

  wait(for: [expectation], timeout: 60.0)
}

Here’s a code breakdown:

  1. First, you create a SKTestSession and configure it with the ReceiptsAndCoins configuration file. You also reset the session to its default state and disable any UI dialogs that might trigger during the session.
  2. Next, you establish the product you want to test buying by using the Tart Cherry Salad’s product ID. You also create an expectation you’ll wait for because the test will run asynchronously.
  3. Then, you execute a purchase of the Tart Cherry Salad using requestProductsAndBuy(productIdentifier:completionHandler:) from IAPHelper instance store. It talks to StoreKit, persists the purchase and updates the app content accordingly.
  4. Finally, after the purchase, you check the receipt validating the purchase was successful.

Those two actions serve the same purpose: Telling Xcode which set of products you want to load into the test environment. In fact, all the StoreKit configuration settings you saw in Editor are configurable in code using SKTestSession‘s properties.

Note: When you manually tested the same purchase flow, you set the ReceiptsAndCoins configuration file in the scheme settings. Here, you set it in SKTestSession‘s initializer.

Those two actions serve the same purpose: Telling Xcode which set of products you want to load into the test environment. In fact, all the StoreKit configuration settings you saw in Editor are configurable in code using SKTestSession‘s properties.

Run your test by clicking the diamond test indicator located to the left of testReceiptValidation(). Alternatively, you can also run the test by pressing Command-U. Your test passes!

Receipt validation test passes

Next, you’ll test first time purchases.

Testing First Time Purchases

It’s important to test first-time purchases to ensure sure your app correctly handles users purchasing new content.

Replace testReceiptValidation() with:

func testNonConsumablePurchase() throws {
  // 1
  let session = try SKTestSession(configurationFileNamed: "RecipesAndCoins")
  session.resetToDefaultState()
  session.disableDialogs = true
  session.clearTransactions()

  // 2
  let identifier = GreenBarContent.caesarSalad
  let expectation = XCTestExpectation(description: "Buy Content")

  GreenBarContent.store.requestProductsAndBuy(
    productIdentifier: identifier
  ) { _ in
    // 3
    let contentAvailable = GreenBarContent.store.receiptContains(identifier)
    let contentSaved = GreenBarContent.store.isProductPurchased(identifier)

    XCTAssertTrue(
      contentAvailable,
      "Expected \(identifier) is present in receipt")
    XCTAssertTrue(
      contentSaved,
      "Expected \(identifier) is stored in PurchasedProducts")

    expectation.fulfill()
  }
  
  wait(for: [expectation], timeout: 60.0)
}

This test is similar to the last. Here’s what your code does:

  1. Like previous test, this test starts by preparing a SKTestSession. However, you’re testing first time purchases so you call clearTransactions() to delete all past transactions and start from scratch.
  2. You set identifier to the Caesar Salad product ID.
  3. Finally, you make sure the receipt contains the product ID. You also make sure the product ID appears in your purchased products list.

Run your test by pressing Command-U. Like before, the green diamond with a checkmark indicates your test passes!

First time purchase test passes

Now, you’ll test interrupted purchases.

Testing Interrupted Purchases

Interrupted purchases are transactions that ask users to perform an action related to their Apple ID before completing. For example, a user might have to update payment information.

To test an interrupted purchase, replace testNonConsumablePurchase() with:

func testInterruptedPurchase() throws {
  let session = try SKTestSession(configurationFileNamed: "RecipesAndCoins")
  session.resetToDefaultState()
  session.disableDialogs = true

  // 1
  session.interruptedPurchasesEnabled = true
  session.clearTransactions()

  let identifier = GreenBarContent.healthyTacoSalad
  let expectation = XCTestExpectation(description: "Buy Content")

  GreenBarContent.store.requestProductsAndBuy(
    productIdentifier: identifier
  ) { _ in
    let contentAvailable = GreenBarContent.store.receiptContains(identifier)
    let contentSaved = GreenBarContent.store.isProductPurchased(identifier)

    // 2
    XCTAssertFalse(
      contentAvailable,
      "Expected \(identifier) is not present in receipt")
    XCTAssertFalse(
      contentSaved,
      "Expected \(identifier) is not stored in PurchasedProducts")

    expectation.fulfill()
  }
  
  wait(for: [expectation], timeout: 60.0)
}

This test is almost identical to the previous one, except for two key differences:

  1. You set SKTestSession‘s interruptedPurchasesEnabled to true to simulate an interrupted purchase.
  2. Because it’s an interrupted purchase you expect the product isn’t in the receipt or your previous purchases.

Run your test with Command-U and watch for the green checkmark.

Interrupted purchase test passes

Finally, it’s time to test subscriptions.

Testing Subscriptions

Your final test exercises GreenBar’s ability to create and expire subscriptions. Replace testInterruptedPurchase() with:

func testSubscription() throws {
  // 1
  let session = try SKTestSession(configurationFileNamed: "Subscriptions")
  session.resetToDefaultState()
  session.disableDialogs = true
  session.clearTransactions()

  // 2
  let identifier = GreenBarContent.greenTimes
  let expectation = XCTestExpectation(description: "Buy Content")

  GreenBarContent.store.requestProductsAndBuy(
    productIdentifier: identifier
  ) { _ in
    // 3
    let contentAvailable = GreenBarContent.store.receiptContains(identifier)
    var contentSaved = GreenBarContent.store.isProductPurchased(identifier)

    XCTAssertTrue(
      contentAvailable,
      "Expected \(identifier) is present in receipt")
    XCTAssertTrue(
      contentSaved,
      "Expected \(identifier) is stored in PurchasedProducts")

    // 4
    var hasExpired = GreenBarContent.store.checkSubscriptionExpiry(identifier)

    XCTAssertFalse(hasExpired, "Expected \(identifier) has not expired")

    // 5
    try? session.expireSubscription(productIdentifier: identifier)

    hasExpired = GreenBarContent.store.checkSubscriptionExpiry(identifier)
    contentSaved = GreenBarContent.store.isProductPurchased(identifier)

    XCTAssertTrue(hasExpired, "Expected \(identifier) has expired")
    XCTAssertFalse(
      contentSaved,
      "Expected \(identifier) is not stored in PurchasedProducts")

    expectation.fulfill()
  }

  wait(for: [expectation], timeout: 90.0)
}
Note: For this final test use a simulator or device running iOS 14.1. I’ve experienced an issue where subscriptionExpirationDate does not change in the local receipt after calling expireSubscription(productIdentifier:) in later iOS versions. This appears still to be a problem in iOS 14.4.

This test looks a little longer but it’s simple! Here’s how it works:

  1. First, you create a new SKTestSession. However, this time you initialize it with your Subscriptions.
  2. Next, you set the product you want to purchase to the Green Times newsletter product ID.
  3. After you purchase the subscription, you check if the receipt shows the Green Times newsletter ID. You also check if the newsletter is available in your purchased products.
  4. Then, you make sure the subscription you set up hasn’t expired.
  5. Last, you expire the subscription and make sure its no longer in your purchased products.

Run your last test and see that it passes.

Subscription test passes