Cannot add SKPaymentTransactionObserver in Unit Test

I have followed the StoreKit test setup guide and it is worked in integrated test.

After that, I want to create unit test about SKPaymentTransactionObserver. I have write the unit test code like below.

import XCTest
import StoreKitTest

@available(iOS 14.0, *)
final class SimpleSKProductTransactionObserverTest: XCTestCase {
  var sut: MockSKProductTransactionObserver!
  var session: SKTestSession!

  override func setUpWithError() throws {
    session = try SKTestSession(configurationFileNamed: "ShopStoreKitConfig")
    session.resetToDefaultState()
    session.disableDialogs = true
    session.clearTransactions()
     
    sut = .init()
     
    SKPaymentQueue.default().add(sut)
  }

  override func tearDownWithError() throws {
    SKPaymentQueue.default().remove(sut)
    sut = nil
    session = nil
  }

  func testPaymentQueue() throws {
    let productId = "prod_sale1_battery_1_day"
     
    try session.buyProduct(productIdentifier: productId)
     
    XCTAssertTrue(sut.isPaymentQueueCalled)
    XCTAssertEqual(sut.previousTransactions.first?.payment.productIdentifier, productId)
  }

}

class MockSKProductTransactionObserver: NSObject, SKPaymentTransactionObserver {
  var previousTransactions = [SKPaymentTransaction]()
  var isPaymentQueueCalled = false
   
  func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    isPaymentQueueCalled = true
    previousTransactions = transactions
  }
}

However, it is failed the assert codes.

/Users/jooyoulkim/git/rp_ios/PickaTests/SimpleSKProductTransactionObserverTest.swift:39 testPaymentQueue(): XCTAssertTrue failed

/Users/jooyoulkim/git/rp_ios/PickaTests/SimpleSKProductTransactionObserverTest.swift:40 testPaymentQueue(): XCTAssertEqual failed: ("nil") is not equal to ("Optional("prod_sale1_battery_1_day")")

The other side, I can see that SKPaymentTransactionObserver added in AppDelegate is worked normally.

So, the question is why observer is not added. Please tell me if have any idea about it. Thanks.

Answered by joo_youl in 743139022

The problem is not that an observer cannot be added but an observer is worked async.

So, it should be written by using expectation.

For using expectation's fulfill, it need to callback after observer is updated or removed transaction.

class ProxySKProductTransactionObserver: NSObject, SKPaymentTransactionObserver {
  var origin: SKPaymentTransactionObserver
  var updatedTransactionsPostCallback: ((SKPaymentQueue, [SKPaymentTransaction]) -> Void)?
  var removedTransactionsPostCallback: ((SKPaymentQueue, [SKPaymentTransaction]) -> Void)?
   
  init(_ origin: SKPaymentTransactionObserver) {
    self.origin = origin
  }
   
  func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    origin.paymentQueue(queue, updatedTransactions: transactions)
    updatedTransactionsPostCallback?(queue, transactions)
  }
   
  func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
    origin.paymentQueue?(queue, removedTransactions: transactions)
    removedTransactionsPostCallback?(queue, transactions)
  }
}

After using expectation, test is passed

import XCTest
import StoreKitTest

@available(iOS 14.0, *)
final class SimpleSKProductTransactionObserverTest: XCTestCase {
  var sut: MockSKProductTransactionObserver!
  var proxy: ProxySKProductTransactionObserver!
  var session: SKTestSession!
   
  override func setUpWithError() throws {
    session = try SKTestSession(configurationFileNamed: "ShopStoreKitConfig")
    session.resetToDefaultState()
    session.disableDialogs = true
    session.clearTransactions()
     
    sut = .init()
    proxy = .init(sut)
     
    SKPaymentQueue.default().add(proxy)
  }
   
  override func tearDownWithError() throws {
    SKPaymentQueue.default().remove(proxy)
    proxy = nil
    sut = nil
    session = nil
  }
   
  func testPaymentQueue() throws {
    let productId = "prod_sale1_battery_1_day"
    let expectation = self.expectation(description: "updatedTransaction is called")
     
    proxy.updatedTransactionsPostCallback = { _, _ in
      expectation.fulfill()
    }
     
    try session.buyProduct(productIdentifier: productId)
     
    waitForExpectations(timeout: 5)
     
    XCTAssertTrue(sut.isPaymentQueueCalled)
    XCTAssertEqual(sut.previousTransactions.first?.payment.productIdentifier, productId)
  }
}

class MockSKProductTransactionObserver: NSObject, SKPaymentTransactionObserver {
  var previousTransactions = [SKPaymentTransaction]()
  var isPaymentQueueCalled = false
   
  func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    isPaymentQueueCalled = true
    previousTransactions = transactions
  }
}
Accepted Answer

The problem is not that an observer cannot be added but an observer is worked async.

So, it should be written by using expectation.

For using expectation's fulfill, it need to callback after observer is updated or removed transaction.

class ProxySKProductTransactionObserver: NSObject, SKPaymentTransactionObserver {
  var origin: SKPaymentTransactionObserver
  var updatedTransactionsPostCallback: ((SKPaymentQueue, [SKPaymentTransaction]) -> Void)?
  var removedTransactionsPostCallback: ((SKPaymentQueue, [SKPaymentTransaction]) -> Void)?
   
  init(_ origin: SKPaymentTransactionObserver) {
    self.origin = origin
  }
   
  func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    origin.paymentQueue(queue, updatedTransactions: transactions)
    updatedTransactionsPostCallback?(queue, transactions)
  }
   
  func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
    origin.paymentQueue?(queue, removedTransactions: transactions)
    removedTransactionsPostCallback?(queue, transactions)
  }
}

After using expectation, test is passed

import XCTest
import StoreKitTest

@available(iOS 14.0, *)
final class SimpleSKProductTransactionObserverTest: XCTestCase {
  var sut: MockSKProductTransactionObserver!
  var proxy: ProxySKProductTransactionObserver!
  var session: SKTestSession!
   
  override func setUpWithError() throws {
    session = try SKTestSession(configurationFileNamed: "ShopStoreKitConfig")
    session.resetToDefaultState()
    session.disableDialogs = true
    session.clearTransactions()
     
    sut = .init()
    proxy = .init(sut)
     
    SKPaymentQueue.default().add(proxy)
  }
   
  override func tearDownWithError() throws {
    SKPaymentQueue.default().remove(proxy)
    proxy = nil
    sut = nil
    session = nil
  }
   
  func testPaymentQueue() throws {
    let productId = "prod_sale1_battery_1_day"
    let expectation = self.expectation(description: "updatedTransaction is called")
     
    proxy.updatedTransactionsPostCallback = { _, _ in
      expectation.fulfill()
    }
     
    try session.buyProduct(productIdentifier: productId)
     
    waitForExpectations(timeout: 5)
     
    XCTAssertTrue(sut.isPaymentQueueCalled)
    XCTAssertEqual(sut.previousTransactions.first?.payment.productIdentifier, productId)
  }
}

class MockSKProductTransactionObserver: NSObject, SKPaymentTransactionObserver {
  var previousTransactions = [SKPaymentTransaction]()
  var isPaymentQueueCalled = false
   
  func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    isPaymentQueueCalled = true
    previousTransactions = transactions
  }
}

I'm trying to do this but SKPaymentQueue.default() doesn't seem to be picking the transaction triggered by  try session.buyProduct(productIdentifier: productId), so the callback is never used, any suggestions ?

This is the example of storekit file. You can save it as file named ShopStoreKitConfig.storekit in your project.

{
 "identifier" : "BB337E99",
 "nonRenewingSubscriptions" : [

 ],
 "products" : [
  {
   "displayPrice" : "4.99",
   "familyShareable" : false,
   "internalID" : "60FA6345",
   "localizations" : [
    {
     "description" : "Unlimited battery for 1 day",
     "displayName" : "Unlimited battery for 1 day",
     "locale" : "en_US"
    }
   ],
   "productID" : "prod_sale1_battery_1_day",
   "referenceName" : “Unlimited battery for 1 day”,
   "type" : "Consumable"
  },
 ],
 "settings" : {

 },
 "subscriptionGroups" : [

 ],
 "version" : {
  "major" : 1,
  "minor" : 1
 }
}
Cannot add SKPaymentTransactionObserver in Unit Test
 
 
Q