The SDK’s injection points exist so tests never need the real store, real network, or persisted state.
Deterministic unit tests
Replace the durable pieces with memory implementations and inject a fake adapter:
import {
type SubKitIapAdapterBundle,
configureSubKit,
createMemoryPurchaseQueueStore,
} from '@piparotech/subkit-expo'
const fakeAdapter: SubKitIapAdapterBundle = {
iap: {
async initConnection() {
return true
},
async endConnection() {},
async fetchProducts() {
return []
},
async getAvailablePurchases() {
return []
},
async requestPurchase() {
return []
},
async finishTransaction() {},
},
listeners: {
addPurchaseErrorListener: () => ({ remove() {} }),
addPurchaseUpdatedListener: () => ({ remove() {} }),
},
}
configureSubKit({
sdkKey: 'sk_sdk_test',
installationId: 'install_test',
apiBaseUrl: 'http://127.0.0.1:3010', // plain HTTP allowed for localhost
adapterBundle: fakeAdapter,
autoStart: false, // control startup explicitly in tests
platform: 'ios',
queue: createMemoryPurchaseQueueStore(),
})Notes:
createMemoryPurchaseQueueStore()keeps queue state in memory — nothing persists between tests.autoStart: falseprevents background sync from racing your assertions; callclient.start()when the test needs it.platformpins platform-dependent behavior instead of relying onPlatform.OS.- Point
apiBaseUrlat a local test server or mock; localhost may use plain HTTP.
What to assert
Focus tests on your app’s decisions, not the SDK’s internals:
- paywall renders only packages with a
storeProduct, - every
PurchaseResultstatus leads to the right UI state, - features unlock only when the Effective Access decision is
granted, - an active different entitlement never grants the requested key,
device_blockedalways includes a typed recovery reason,- local native state cannot override an inactive/loading SubKit decision,
- restore re-checks
getAccess(key)afterward.
Offline evaluation without a device
evaluateOfflineCustomerInfo(info, { now }) applies the offline expiry rules
to a CustomerInfo value — useful for testing expiry behavior with controlled
clocks:
import { resolveEntitlementAccess } from '@piparotech/subkit-core'
import { evaluateOfflineCustomerInfo } from '@piparotech/subkit-expo'
const offline = evaluateOfflineCustomerInfo(cachedInfo, { now: Date.now() })
const access = resolveEntitlementAccess(offline, 'pro')
expect(access.state).toBe('inactive') // past expiresAtSandbox and test purchases
Real end-to-end purchase verification needs the providers’ test environments:
- iOS: a development build plus a sandbox tester account. Sandbox purchases
are verified by SubKit with
environment: sandbox— the signedaccessContextkeeps sandbox and production reads separated. - Android: a testing-track build plus license testers in Play Console.
Never put real keys in test fixtures; test SDK keys are still public app-bound keys, and server keys never appear in app or test code.
Use the provider guides for the full server-side and console setup: