工程檔案

用 StoreKit 設定建立可重複的 iOS 內購回歸測試

用 StoreKit 設定建立可重複的 iOS 內購回歸測試

內購狀態機最難重現的通常不是「購買成功」,而是取消、待處理、退款,以及上一筆交易尚未完成的情況。若直接交由外部交易環境處理這些分支,網路、商品設定與測試帳號狀態都可能影響結果。更穩定的做法,是先在雲端 Mac 上透過 StoreKit 設定檔固定商品,再使用 SKTestSession 控制交易條件,將用戶端邏輯轉化為可重複執行的回歸測試。

先釐清離線測試的邊界

StoreKit 設定適合驗證商品識別碼對應、購買狀態轉換、權益更新、錯誤提示與交易完成邏輯。它不需要存取外部付款流程,發生失敗時也更容易重現。

但它無法證明線上商品已正確設定,也無法涵蓋伺服器通知、真實收據驗證與最終付款介面。建議將測試分成三層:

層級 主要目標 執行頻率
單元測試 權益計算、狀態對應 每次提交
StoreKit 整合測試 購買、取消、退款、未完成交易 每次合併
外部環境驗收 商品設定、通知與收據流程 發布前

離線測試的價值在於縮短回饋週期,而不是把所有交易風險偽裝成一個綠色勾號。

固定商品與測試進入點

在 Xcode 中建立 StoreKit Configuration File,只保留測試所需的商品。商品識別碼應與程式碼常數一致,價格僅用於介面測試,不要讓業務邏輯依據本地化價格字串判斷權益。

將設定檔加入測試 Scheme,並建立獨立的測試計畫,例如 StoreKitRegression.xctestplan。不要重複使用開發者日常執行的 Scheme,否則某次手動修改可能在不知情的情況下改變 CI 條件。

測試 Target 可在 setUpWithError() 中重新建立工作階段:

import StoreKitTest
import XCTest

final class PurchaseRegressionTests: XCTestCase {
    private var session: SKTestSession!

    override func setUpWithError() throws {
        session = try SKTestSession(configurationFileNamed: "Products")
        session.disableDialogs = true
        session.clearTransactions()
        session.failTransactionsEnabled = false
    }

    override func tearDownWithError() throws {
        session.clearTransactions()
        session = nil
    }
}

設定檔名稱不含副檔名。若初始化失敗,請先確認檔案屬於測試 Target,並檢查 Scheme 是否使用同一份設定。

將交易分支拆成獨立測試案例

不要在一個冗長的測試案例中連續測試成功、退款與取消。狀態越多,失敗後就越難判斷污染是在哪個步驟發生。至少應拆分出以下案例:

正常購買與恢復

驗證購買後權益會立即更新,重新建立業務層物件後仍能讀取有效權益。測試結束前,確認應用程式已處理並完成交易,避免下一個測試案例再次收到舊的更新。

取消與失敗注入

取消不應顯示「付款失敗」,也不應寫入已解鎖狀態。網路或一般交易錯誤則應保留重試入口。使用工作階段屬性注入錯誤後,必須在目前測試結束前恢復預設值,不能依賴下一個測試案例進行清理。

退款與未完成交易

退款後應撤銷對應權益,但不要誤刪其他仍然有效的商品。對於未完成交易,應驗證應用程式重新啟動後仍可繼續處理,而不是只檢查一次按鈕回呼。

涉及同一商品狀態的測試建議循序執行。若平行測試共用模擬器與設定,很容易彼此清除交易,造成只在 CI 中出現的偶發失敗。

使用命令列固定執行條件

先查看可用的模擬器,再將裝置名稱、系統版本與 Xcode 路徑寫入 CI 變數。不要讓 xcodebuild 自動選擇任意目標。

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
RESULT_DIR="$PWD/TestResults"
rm -rf "$RESULT_DIR"
mkdir -p "$RESULT_DIR"

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme App-StoreKitTests \
  -testPlan StoreKitRegression \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -resultBundlePath "$RESULT_DIR/StoreKit.xcresult"

若在 OakVM 上同時執行多條 Pipeline,應為每個工作分配獨立的工作目錄與模擬器,不要共用 DerivedData、結果目錄或已啟動的裝置。開始前可以執行 xcrun simctl shutdown all,但這會影響同一節點上的其他工作,因此只適合由單一工作獨佔節點的執行方式。

封存證據並設定失敗門檻

測試失敗後,通常只保留主控台末尾數十行內容並不足夠。應封存 xcresult、提交雜湊值、StoreKit 設定檔校驗值、Xcode 版本與模擬器執行階段版本。設定檔異動時,也必須納入程式碼審查,避免商品識別碼遭到意外刪除。

可以先記錄環境摘要:

xcodebuild -version
xcrun simctl list runtimes
shasum -a 256 Tests/StoreKit/Products.storekit

檢查清單應包括:

  • 每個測試是否都建立並清理自己的 SKTestSession
  • 是否關閉自動彈出視窗,避免無人值守的工作遭到阻塞
  • 是否分別針對取消、失敗與退款斷言介面狀態及權益狀態
  • 測試是否依賴執行順序或上一筆交易
  • xcresult 是否在失敗後仍會上傳
  • 日誌是否避開敏感憑證與完整交易資料

固定這些條件後,內購回歸測試便不再依賴手動點擊。外部環境只需負責它真正需要驗證的部分,日常提交則由快速、隔離且可追溯的測試守住用戶端狀態機。

常見問題

StoreKit 離線測試可以完全取代真實交易環境驗收嗎?

不可以。它適合驗證用戶端狀態機、商品對應與錯誤分支,但實際商品設定、伺服器通知、收據處理及付款介面仍須另外驗收。

為什麼內購測試單獨執行成功,放進完整測試套件卻偶爾失敗?

先檢查交易狀態是否殘留。每個測試應建立新的 SKTestSession、清除交易並停用對話框,會修改同一商品狀態的案例則應循序執行。

內購回歸失敗時至少要保存哪些證據?

至少保存 xcresult、測試日誌、StoreKit 設定檔版本、提交雜湊、Xcode 版本與模擬器執行環境版本,且不得記錄敏感憑據。

OAKVM BUILD NODE

在獨享實體節點上執行下一次建置

選擇 OakVM M4 或 OakVM M4 Pro,按日、週、月或季租用非虛擬化的雲端 Mac。節點的實際可用狀態以控制台即時回傳結果為準。

選擇設定並租用