Skip to content

Testing

Test your Drop-in integration in sandbox before going live.

Overview

Test your Drop-in integration in the sandbox environment before accepting real payments. The sandbox environment allows you to test all payment methods without processing actual transactions.

Use the test environment

Set the environment to .test when initialising Drop-in:

CheckoutDropInConfig(
    environment: .test,  // Use sandbox
    // ... rest of config
)

Key points about sandbox testing:

  • No real money is charged.
  • Use test card numbers and sandbox accounts.
  • Payment methods behave like production, with the same SDK constraints.
  • Backend verification still required using test API credentials.

Test card payments

Test card numbers

Use the following test cards in sandbox to simulate different scenarios.

The test card numbers below are examples. Actual available test cards depend on your merchant and test processor configuration.

Successful payments

The following test cards represent successful payment scenarios:

Card numberNetwork3DS behaviour
4111 1111 1111 1111VisaNo challenge
5555 5555 5555 4444MastercardNo challenge
3782 822463 10005American ExpressNo challenge
4000 0000 0000 0002VisaChallenge required

For all test cards:

  • Expiry date: Any future date (e.g., 12/25).
  • CVV: Any 3 digits (e.g., 123).
  • Cardholder name: Any name.

Failed payments

The following test cards simulate payment failures:

Card numberNetworkResult
4000 0000 0000 0077VisaInsufficient funds
4000 0000 0000 0051VisaCard declined
4000 0000 0000 0069VisaExpired card
4000 0000 0000 0101VisaCVV failure

Testing 3DS authentication

When using the challenge test card (4000 0000 0000 0002), a 3D Secure authentication screen appears. The exact 3DS flow depends on your payment processor and card network provider.

  • To pass: enter any code (e.g., 1234).
  • To fail: leave empty or enter wrong code multiple times.
  • To timeout: wait for authentication to timeout.

Expected callback behaviour:

  • Success: onSuccess callback fires with DropInSubmitResult containing transaction details.
  • Failure: onError callback fires with the authentication error (e.g., SDK1114 for authentication failed).

Verify that your backend receives the transaction details after successful authentication.

Test PayPal

PayPal sandbox accounts

You need a PayPal sandbox account for testing. Create one in the PayPal Developer Dashboard.

Create a personal (buyer) account for testing customer payments.

Test PayPal payments

The PayPal button only appears when session.allowedFundingTypes.wallets.paypal.allowedFundingOptions is present and non-empty in your session configuration.

  1. Tap the PayPal button in the drop-in.
  2. Log in with your PayPal sandbox account in the web view.
  3. Approve the payment.
  4. Verify that onSuccess fires with transaction details.
  5. Verify your backend receives the payment notification.

Test both transaction intents:

  • Purchase intent: Funds captured immediately.
  • Authorisation intent: Funds authorised but not captured (capture later via backend API).

Test Apple Pay

Requirements

  • iOS 14.0 or higher
  • Apple Pay capability enabled in Xcode
  • Valid Apple Pay merchant identifier
  • Test card added to Apple Wallet

Test Apple Pay payments

  1. Open your app on an iOS device or simulator.
  2. Ensure Apple Pay is set up with test cards.
  3. Add a test card to Apple Wallet (use test card numbers from above).
  4. Tap the Apple Pay button in Drop-in.
  5. Authenticate with Face ID, Touch ID, or passcode.
  6. Verify that onSuccess fires with transaction details.

The Apple Pay button only appears when:

  • The user has cards in their Apple Wallet and Apple Pay is properly configured.
  • session.allowedFundingTypes.wallets.applePay.merchantId is present in your session configuration.

Testing on simulator

To test Apple Pay on an iOS simulator:

  1. Use Simulator with iOS 14.0 or higher.
  2. In the Simulator menu, click Wallet > Add Credit or Debit Card.
  3. Add test cards manually using the card numbers above.
  4. Run your app and test Apple Pay integration.

Apple Pay sandbox testing requires proper Xcode project configuration with the Apple Pay capability and a valid merchant identifier registered in your Apple Developer account.

Test error handling

Test that your error handling works correctly by simulating failures:

Card errors

Use the failed payment test cards to trigger different error types:

  • Insufficient funds: 4000 0000 0000 0077
  • Card declined: 4000 0000 0000 0051
  • Expired card: 4000 0000 0000 0069
  • CVV failure: 4000 0000 0000 0101

Verify that your onError callback receives the error and displays an appropriate message to the customer.

Expected SDK error codes

Your onError callback may receive the following SDK error codes:

General payment errors

  • SDK0204: No payment methods available. Drop-in calls onError with paymentMethod: nil during create() when no payment methods can be rendered (an inline noPaymentMethodsError message is still shown). To test in sandbox, use a session with no wallets and DropInCardConfig(showCOF: false, showNewCard: false).
  • SDK1114: Authentication failed (3DS or similar authentication failure).
  • SDK1115: Authorisation failed (payment declined by issuer/processor).
  • SDK1116: Card payment failed (general card payment error).
  • SDK1117: PayPal payment failed (general PayPal error).
  • SDK1119: Apple Pay payment failed (general Apple Pay error).
  • SDK1120: Invalid PayPal entry type (configuration mismatch).

Apple Pay-specific errors

  • SDK0602: Apple Pay configuration error.
  • SDK0615: Apple Pay session cancelled by the user.
  • SDK0616: Apple Pay merchant validation failed.
  • SDK0617: Custom API processing failed during Apple Pay payment.

Network errors

Test offline behaviour:

  1. Enable airplane mode on your device.
  2. Try to submit a payment.
  3. Verify that your error handling shows a network error message.

Alternatively, use Network Link Conditioner in Xcode or your app's backend/mock layer to simulate network failures.

// Pseudocode example - adapt to your app's architecture
// Use your app's backend mock layer or dependency injection
// to simulate network failures in tests
class NetworkErrorTest: XCTestCase {
    func testNetworkError() async throws {
        // Configure your backend mock to return network errors
        // or use URLProtocol/URLSession mocks in your app
        
        // Verify error handling in your app
        // ... test implementation
    }
}

User cancellation

To test cancellation handling:

  • PayPal: close the web view without completing payment. This doesn't trigger methodConfig.global.onCancel because Drop-in overrides the PayPal cancel handler with an internal handler to clear processing state.
  • Apple Pay: tap outside the payment sheet or cancel the authentication. This triggers methodConfig.global.onCancel(.applePay, error) if configured.

Test backend verification

Always verify payments on your backend before fulfilling orders. The frontend callbacks can be manipulated by users.

Essential backend tests

Test these critical security checks:

  1. Transaction exists: query the PXP API with systemTransactionId to verify that the transaction exists.
  2. Amount matches: verify that the transaction amount matches your expected amount.
  3. Transaction succeeded: check that the transaction state is Authorised or Captured.
  4. Prevent replay attacks: ensure the same transaction can't be processed twice.

Example verification:

// Backend verification endpoint (Node.js/Express example)
app.post('/api/verify-payment', async (req, res) => {
  const { systemTransactionId, merchantTransactionId } = req.body;
  
  // Get expected amount from your database
  const order = await db.orders.findOne({ merchantTransactionId });
  
  // Guard against missing order
  if (!order) {
    return res.status(404).json({ success: false, error: 'Order not found' });
  }
  
  const expectedAmount = order.amount;
  
  // Query PXP API to verify transaction
  const transaction = await unityApi.getTransaction(systemTransactionId);
  
  // Verify transaction is successful
  if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
    return res.json({ success: false, error: 'Transaction not authorised' });
  }
  
  // Verify amount matches
  const txnAmount = transaction.amounts?.transactionValue || transaction.amount;
  if (Math.abs(txnAmount - expectedAmount) > 0.01) {
    return res.json({ success: false, error: 'Amount mismatch' });
  }
  
  // Check for duplicate processing
  const alreadyProcessed = await db.transactions.findOne({ systemTransactionId });
  if (alreadyProcessed) {
    return res.json({ success: true, orderId: alreadyProcessed.orderId });
  }
  
  // Process order
  const orderId = await fulfillOrder(order);
  await db.transactions.create({ systemTransactionId, orderId });
  
  res.json({ success: true, orderId });
});

Test on physical devices

Test on real iOS devices to ensure the best user experience:

Test on devices that represent your user base:

  • Budget device: iPhone SE (2nd/3rd generation)
  • Standard device: iPhone 13, iPhone 14
  • Pro device: iPhone 15 Pro, iPhone 14 Pro
  • Tablet: iPad Air, iPad Pro

Device-specific testing

Test the following on each device:

  • Card field rendering and keyboard input.
  • 3D Secure authentication UI.
  • Apple Pay integration and authentication.
  • PayPal web view interaction.
  • Screen rotation handling.
  • Different screen sizes (iPhone vs iPad).
  • Performance and responsiveness.

Screen size testing

Test on different screen sizes:

// Use SwiftUI Previews for different screen sizes
struct CheckoutView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            CheckoutView()
                .previewDevice(PreviewDevice(rawValue: "iPhone SE (3rd generation)"))
                .previewDisplayName("iPhone SE")
            
            CheckoutView()
                .previewDevice(PreviewDevice(rawValue: "iPhone 15 Pro"))
                .previewDisplayName("iPhone 15 Pro")
            
            CheckoutView()
                .previewDevice(PreviewDevice(rawValue: "iPad Pro (12.9-inch)"))
                .previewDisplayName("iPad Pro")
        }
    }
}

Automated testing

Write automated tests for your Drop-in integration:

Unit tests

Test your configuration and callback logic. Card-only examples below use GBP.

import XCTest
import PXPCheckoutSDK

class CheckoutConfigTests: XCTestCase {
    func testConfigCreation() {
        let config = CheckoutDropInConfig(
            environment: .test,
            session: mockSessionData(),
            transactionData: DropInTransactionData(
                amount: Decimal(string: "99.99") ?? 0,
                currency: "GBP",
                entryType: .ecom,
                intent: DropInTransactionIntentData(
                    card: .authorisation
                ),
                merchantTransactionId: "test-txn-123",
                merchantTransactionDate: { Date() }
            ),
            merchantShopperId: "shopper-123",
            ownerId: "MERCHANT-1"
        )
        
        XCTAssertEqual(config.environment, .test)
        XCTAssertEqual(config.ownerId, "MERCHANT-1")
        XCTAssertEqual(config.transactionData.currency, "GBP")
    }
    
    func testCallbackExecution() {
        var successCalled = false
        var errorCalled = false
        
        let config = CheckoutDropInConfig(
            environment: .test,
            session: mockSessionData(),
            transactionData: DropInTransactionData(
                amount: Decimal(string: "99.99") ?? 0,
                currency: "GBP",
                entryType: .ecom,
                intent: DropInTransactionIntentData(
                    card: .authorisation
                ),
                merchantTransactionId: "test-txn-callback-123",
                merchantTransactionDate: { Date() }
            ),
            merchantShopperId: "shopper-callback-123",
            ownerId: "MERCHANT-1",
            onSuccess: { result in
                Task { @MainActor in
                    successCalled = true
                }
            },
            onError: { paymentMethod, error in
                Task { @MainActor in
                    errorCalled = true
                }
            }
        )
        
        // Test callbacks
        config.onSuccess?(mockSuccessResult())
        XCTAssertTrue(successCalled)
        XCTAssertFalse(errorCalled)
    }
    
    func mockSessionData(includePaze: Bool = false) -> SessionData {
        return SessionData(
            sessionId: "test-session-id",
            hmacKey: "test-hmac-key",
            encryptionKey: "test-encryption-key",
            sessionExpiry: nil,
            allowedFundingTypes: AllowedFundingType(
                cardSchemes: ["Visa", "Mastercard"],
                cards: [],
                wallets: Wallets(
                    paypal: Paypal(
                        allowedFundingOptions: ["paypal"],
                        merchantId: "test-merchant-id"
                    ),
                    applePay: ApplePay(merchantId: "merchant.test.app"),
                    paze: includePaze
                        ? Paze(clientId: "test-paze-client-id", merchantCategoryCode: "5812")
                        : nil
                )
            ),
            restrictions: nil
        )
    }
    
    func testPazeConfigUsesUSD() {
        let config = CheckoutDropInConfig(
            environment: .test,
            session: mockSessionData(includePaze: true),
            transactionData: DropInTransactionData(
                amount: Decimal(string: "49.99") ?? 0,
                currency: "USD",
                entryType: .ecom,
                intent: DropInTransactionIntentData(card: .authorisation),
                merchantTransactionId: "test-paze-txn-123",
                merchantTransactionDate: { Date() }
            ),
            merchantShopperId: "shopper-123",
            ownerId: "MERCHANT-1",
            onSuccess: { _ in },
            onError: { _, _ in }
        )
        
        XCTAssertEqual(config.transactionData.currency, "USD")
        XCTAssertEqual(config.transactionData.entryType, .ecom)
    }
    
    func mockSuccessResult(paymentMethod: DropInPaymentMethod = .card) -> DropInSubmitResult {
        return DropInSubmitResult(
            systemTransactionId: "sys-txn-123",
            merchantTransactionId: "test-txn-123",
            paymentMethod: paymentMethod
            // paymentData is nil for card, PayPal, and Apple Pay; optional .paze for Paze
        )
    }
}

Async Drop-in test pattern

Test async Drop-in initialisation:

class AsyncDropInTests: XCTestCase {
    func testAsyncDropInCreation() async throws {
        let config = CheckoutDropInConfig(
            environment: .test,
            session: mockSessionData(),
            transactionData: DropInTransactionData(
                amount: Decimal(string: "49.99") ?? 0,
                currency: "GBP",
                entryType: .ecom,
                intent: DropInTransactionIntentData(
                    card: .authorisation
                ),
                merchantTransactionId: "test-async-txn-123",
                merchantTransactionDate: { Date() }
            ),
            merchantShopperId: "shopper-async-123",
            ownerId: "MERCHANT-1"
        )
        
        // Test async initialisation
        let dropIn = try CheckoutDropIn(config: config)
        await dropIn.create()
        
        // Verify Drop-in is ready
        XCTAssertNotNil(dropIn)
    }
}

UI tests

Test the full payment flow:

The UI test selectors shown below (e.g., "Card", "PayPal", "Card number") are examples and aren't guaranteed by the SDK. The SDK may change button labels, field names, or accessibility identifiers in future releases. For robust UI testing, use your merchant app's own stable accessibility identifiers rather than relying on localised text or SDK-provided labels.

import XCTest

class CheckoutFlowUITests: XCTestCase {
    var app: XCUIApplication!
    
    override func setUp() {
        super.setUp()
        continueAfterFailure = false
        app = XCUIApplication()
        app.launch()
    }
    
    func testCardPaymentFlow() {
        // Wait for Drop-in to render
        let cardButton = app.buttons["Card"]
        XCTAssertTrue(cardButton.waitForExistence(timeout: 5))
        
        // Verify payment methods are displayed
        XCTAssertTrue(app.buttons["Card"].exists)
        XCTAssertTrue(app.buttons["PayPal"].exists)
        // If Paze is enabled in your session:
        // XCTAssertTrue(app.buttons["Paze"].exists)
        
        // Select card payment
        cardButton.tap()
        
        // Verify card form is displayed
        XCTAssertTrue(app.textFields["Card number"].exists)
        XCTAssertTrue(app.textFields["Expiry date"].exists)
        XCTAssertTrue(app.textFields["CVV"].exists)
    }
    
    func testPaymentSubmission() {
        let cardButton = app.buttons["Card"]
        XCTAssertTrue(cardButton.waitForExistence(timeout: 5))
        
        // Fill card details and submit
        cardButton.tap()
        
        let cardNumberField = app.textFields["Card number"]
        cardNumberField.tap()
        cardNumberField.typeText("4111111111111111")
        
        let expiryField = app.textFields["Expiry date"]
        expiryField.tap()
        expiryField.typeText("1225")
        
        let cvvField = app.textFields["CVV"]
        cvvField.tap()
        cvvField.typeText("123")
        
        // Submit payment
        app.buttons["Pay"].tap()
        
        // Verify success screen appears
        let successMessage = app.staticTexts["Payment successful"]
        XCTAssertTrue(successMessage.waitForExistence(timeout: 10))
    }
}

Pre-production checklist

Before going live, verify the following:

Configuration

  • Change the environment to .live.
  • Use production API credentials.
  • Verify that the ownerId is correct.
  • Confirm the currency and amount formatting.
  • Verify that all required session fields are present and valid:
    • sessionId: Valid session identifier from your backend.
    • hmacKey: Valid HMAC key for request signing.
    • encryptionKey: Valid encryption key for sensitive data.
    • allowedFundingTypes.cardSchemes and/or allowedFundingTypes.cards: At least one non-empty list of enabled card networks (or set acceptedCardNetworks in DropInGlobalConfig). The SDK resolves networks in priority order: merchant acceptedCardNetworks → session cardSchemes → session cards.
    • allowedFundingTypes.wallets.paypal.allowedFundingOptions: Present and non-empty if PayPal is enabled.
    • allowedFundingTypes.wallets.applePay.merchantId: Present if Apple Pay is enabled.
    • restrictions: Correct merchant-specific restrictions (if any).

Testing

  • Test all enabled payment methods.
  • Test successful payments.
  • Test failed payments and error handling.
  • Verify backend verification works correctly.
  • Test on physical devices (not just simulators).

Security

  • Backend verification implemented.
  • Replay attack prevention in place.
  • HTTPS enabled on all backend endpoints.
  • No sensitive data logged.
  • Code signing and provisioning profiles configured for production.

User experience

  • Test on different screen sizes and orientations.
  • Loading states display correctly.
  • Success screen implemented.
  • Error messages are user-friendly.
  • Accessibility features tested (VoiceOver, Dynamic Type).

Performance

  • App startup time is acceptable.
  • Payment submission is responsive.
  • No memory leaks detected (use Xcode Instruments).
  • Network timeouts configured appropriately.

Code quality

  • SwiftLint checks pass.
  • Unit tests pass.
  • UI tests pass.
  • Code review completed.
  • Documentation updated.