Mobile payment systems have revolutionized how users conduct transactions on smartphones and tablets. From contactless payments with Apple Pay and Google Pay to in-app purchases and subscription models, mobile payment testing requires specialized knowledge of security standards, platform-specific implementations, and compliance requirements. This comprehensive guide covers everything QA engineers need to know about testing mobile payment systems effectively.

Introduction to Mobile Payment Testing Challenges

Mobile payment testing presents unique challenges that differ significantly from traditional web payment testing. The complexity stems from multiple factors:

Platform Fragmentation: Different operating systems (iOS, Android) (as discussed in Cross-Platform Mobile Testing: Strategies for Multi-Device Success) (as discussed in Appium 2.0: New Architecture and Cloud Integration for Modern Mobile Testing) implement payment systems differently, requiring platform-specific testing approaches.

Security Requirements: Payment systems must comply with PCI DSS (Payment Card Industry Data Security Standard), requiring rigorous security testing and data protection measures.

Sandbox Limitations: Test environments often have restricted functionality compared to production, making comprehensive testing challenging.

Third-Party Dependencies: Payment gateways, banks, and platform providers introduce external dependencies that can affect testing reliability.

User Experience Criticality: Payment flows must be seamless and intuitive, as friction in the payment process directly impacts conversion rates and revenue.

Key challenges include:

  • Testing without exposing real payment credentials
  • Validating encryption and secure data transmission
  • Simulating various payment scenarios (as discussed in Detox: Grey-Box Testing for React Native Applications) (success, failure, timeout)
  • Testing across different devices, OS versions, and network conditions
  • Ensuring compliance with regional payment regulations
  • Validating refund and cancellation workflows
  • Testing subscription and recurring payment logic

Apple Pay Testing and Integration

Apple Pay integration requires specific testing strategies due to its ecosystem constraints and security model.

Setting Up Apple Pay Test Environment

Apple provides sandbox environments for testing Apple Pay transactions:

// Enable sandbox mode in iOS app
#if DEBUG
    let configuration = PKPaymentAuthorizationConfiguration()
    configuration.merchantIdentifier = "merchant.com.yourapp.sandbox"
    configuration.supportedNetworks = [.visa, .masterCard, .amex]
    configuration.merchantCapabilities = .capability3DS
#else
    // Production configuration
#endif

Test Card Configuration

Apple provides test cards through the Sandbox environment:

Card TypeTest Card NumberExpected Behavior
Visa4111111111111111Successful payment
Mastercard5555555555554444Successful payment
Amex378282246310005Successful payment
Declined4000000000000002Declined transaction

Critical Test Scenarios for Apple Pay

1. Payment Authorization Flow

func testApplePayAuthorization() {
    let request = PKPaymentRequest()
    request.merchantIdentifier = "merchant.com.yourapp"
    request.supportedNetworks = [.visa, .masterCard, .amex]
    request.merchantCapabilities = .capability3DS
    request.countryCode = "US"
    request.currencyCode = "USD"

    let item = PKPaymentSummaryItem(label: "Test Product", amount: NSDecimalNumber(string: "9.99"))
    request.paymentSummaryItems = [item]

    // Verify authorization controller presentation
    let controller = PKPaymentAuthorizationController(paymentRequest: request)
    controller.delegate = self
    controller.present { presented in
        XCTAssertTrue(presented, "Payment controller should present")
    }
}

2. Biometric Authentication Testing

Test Apple Pay with Face ID/Touch ID:

  • Valid biometric authentication
  • Failed authentication attempts
  • Fallback to passcode entry
  • Disabled biometric settings

3. Device Compatibility Testing

Verify Apple Pay support across:

  • iPhone models (iPhone 6 and later)
  • Apple Watch integration
  • iPad models with Touch ID/Face ID
  • Mac with Touch ID

Apple Pay Validation Checklist

  • Sandbox environment configured correctly
  • Test merchant identifier active
  • Payment sheet displays correct amount and merchant name
  • Supported card networks properly configured
  • 3D Secure authentication working
  • Transaction receipt generated correctly
  • Error handling for declined payments
  • Network timeout scenarios handled
  • Shipping address validation (if applicable)
  • Contact information collection working

Google Pay Testing Strategies

Google Pay offers a more flexible testing environment compared to Apple Pay, with extensive sandbox capabilities.

Google Pay Test Environment Setup

// Android Google Pay configuration
private fun createPaymentsClient(): PaymentsClient {
    val walletOptions = Wallet.WalletOptions.Builder()
        .setEnvironment(WalletConstants.ENVIRONMENT_TEST) // Use test environment
        .build()
    return Wallet.getPaymentsClient(this, walletOptions)
}

private fun createPaymentDataRequest(): PaymentDataRequest {
    val request = PaymentDataRequest.fromJson(
        """
        {
            "apiVersion": 2,
            "apiVersionMinor": 0,
            "allowedPaymentMethods": [
                {
                    "type": "CARD",
                    "parameters": {
                        "allowedAuthMethods": ["PAN_ONLY", "CRYPTOGRAM_3DS"],
                        "allowedCardNetworks": ["MASTERCARD", "VISA"]
                    },
                    "tokenizationSpecification": {
                        "type": "PAYMENT_GATEWAY",
                        "parameters": {
                            "gateway": "example",
                            "gatewayMerchantId": "exampleMerchantId"
                        }
                    }
                }
            ],
            "merchantInfo": {
                "merchantId": "TEST_MERCHANT_ID",
                "merchantName": "Test Merchant"
            },
            "transactionInfo": {
                "totalPriceStatus": "FINAL",
                "totalPrice": "12.34",
                "currencyCode": "USD"
            }
        }
        """.trimIndent()
    )
    return request
}

Google Pay Test Cards

Google provides comprehensive test cards for various scenarios:

ScenarioTest CardCVVZIP
Success411111111111111112312345
Declined - Insufficient Funds400000000000999512312345
Declined - Lost Card400000000000998712312345
3D Secure Authentication400000000000322012312345

Key Google Pay Test Scenarios

1. Payment Method Availability

@Test
fun testGooglePayAvailability() {
    val isReadyToPayRequest = IsReadyToPayRequest.fromJson(
        """
        {
            "apiVersion": 2,
            "apiVersionMinor": 0,
            "allowedPaymentMethods": [
                {
                    "type": "CARD",
                    "parameters": {
                        "allowedAuthMethods": ["PAN_ONLY", "CRYPTOGRAM_3DS"],
                        "allowedCardNetworks": ["MASTERCARD", "VISA"]
                    }
                }
            ]
        }
        """.trimIndent()
    )

    paymentsClient.isReadyToPay(isReadyToPayRequest)
        .addOnCompleteListener { task ->
            assertTrue("Google Pay should be available", task.isSuccessful)
        }
}

2. Token Validation

Verify payment token structure and encryption:

fun validatePaymentToken(token: String) {
    val jsonToken = JSONObject(token)

    // Verify required fields
    assertTrue(jsonToken.has("signature"))
    assertTrue(jsonToken.has("protocolVersion"))
    assertTrue(jsonToken.has("signedMessage"))

    // Verify signature validity
    val signedMessage = jsonToken.getString("signedMessage")
    assertNotNull(signedMessage)
    assertFalse(signedMessage.isEmpty())
}

In-App Purchase (IAP) Testing for iOS and Android

In-app purchases require platform-specific testing approaches for consumables, non-consumables, subscriptions, and auto-renewable content.

iOS In-App Purchase Testing

Sandbox Tester Configuration:

  1. Create sandbox test accounts in App Store Connect
  2. Configure different account types (new users, existing subscribers)
  3. Test with various account regions for localization
// iOS StoreKit testing
import StoreKit

class IAPManager: NSObject, SKPaymentTransactionObserver {
    func purchaseProduct(productId: String) {
        guard SKPaymentQueue.canMakePayments() else {
            print("User cannot make payments")
            return
        }

        let payment = SKPayment(product: product)
        SKPaymentQueue.default().add(payment)
    }

    func paymentQueue(_ queue: SKPaymentQueue,
                     updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchased:
                // Verify receipt and unlock content
                completeTransaction(transaction)
            case .failed:
                // Handle failure
                failedTransaction(transaction)
            case .restored:
                // Restore previous purchase
                restoreTransaction(transaction)
            case .deferred:
                // Payment pending approval
                print("Payment deferred")
            case .purchasing:
                print("Purchasing...")
            @unknown default:
                break
            }
        }
    }
}

iOS IAP Test Cases:

  • Product retrieval from App Store
  • Purchase flow completion
  • Receipt validation (local and server-side)
  • Transaction restoration
  • Interrupted transactions (app termination during purchase)
  • Family Sharing support
  • Promotional offers and introductory pricing

Android In-App Billing Testing

Google Play Billing Library provides comprehensive testing tools:

class BillingManager(private val context: Context) : PurchasesUpdatedListener {
    private lateinit var billingClient: BillingClient

    fun initializeBilling() {
        billingClient = BillingClient.newBuilder(context)
            .setListener(this)
            .enablePendingPurchases()
            .build()

        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(result: BillingResult) {
                if (result.responseCode == BillingClient.BillingResponseCode.OK) {
                    // Query available products
                    queryProducts()
                }
            }

            override fun onBillingServiceDisconnected() {
                // Retry connection
            }
        })
    }

    override fun onPurchasesUpdated(result: BillingResult, purchases: List<Purchase>?) {
        when (result.responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                purchases?.forEach { purchase ->
                    if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
                        // Acknowledge purchase
                        acknowledgePurchase(purchase)
                    }
                }
            }
            BillingClient.BillingResponseCode.USER_CANCELED -> {
                // Handle user cancellation
            }
            else -> {
                // Handle other errors
            }
        }
    }
}

Android Test Purchase IDs:

Google provides reserved product IDs for testing:

  • android.test.purchased - Always successful purchase
  • android.test.canceled - Simulates cancelled purchase
  • android.test.refunded - Simulates refunded purchase
  • android.test.item_unavailable - Simulates unavailable product

Payment Gateway Integration Testing

Payment gateway integration requires testing communication between your app, backend server, and payment processor.

Payment Gateway Test Checklist

Integration Points to Test:

// Backend payment processing example (Node.js)
const stripe = require('stripe')(process.env.STRIPE_TEST_KEY);

async function processPayment(paymentData) {
    try {
        // Create payment intent
        const paymentIntent = await stripe.paymentIntents.create({
            amount: paymentData.amount,
            currency: 'usd',
            payment_method_types: ['card'],
            metadata: {
                orderId: paymentData.orderId,
                userId: paymentData.userId
            }
        });

        // Confirm payment
        const confirmedPayment = await stripe.paymentIntents.confirm(
            paymentIntent.id,
            {
                payment_method: paymentData.paymentMethodId
            }
        );

        return {
            success: true,
            transactionId: confirmedPayment.id,
            status: confirmedPayment.status
        };
    } catch (error) {
        return {
            success: false,
            error: error.message,
            code: error.code
        };
    }
}

Common Payment Gateways and Test Environments

GatewayTest Mode SetupTest Cards Provided
StripeUse test API keys with sk_test_ prefixComprehensive test card suite
PayPalPayPal Sandbox environmentSandbox accounts required
BraintreeSandbox merchant IDTest card numbers provided
SquareSandbox access tokenTest cards for various scenarios
AdyenTest merchant accountTest card numbers per region

API Integration Test Scenarios

# Python integration test example
import unittest
import requests

class PaymentGatewayTests(unittest.TestCase):
    def setUp(self):
        self.base_url = "https://api.payment-gateway.test"
        self.api_key = "test_api_key_12345"

    def test_successful_payment(self):
        """Test successful payment processing"""
        payload = {
            "amount": 1000,  # Amount in cents
            "currency": "USD",
            "card": {
                "number": "4242424242424242",
                "exp_month": 12,
                "exp_year": 2025,
                "cvc": "123"
            }
        }

        response = requests.post(
            f"{self.base_url}/charges",
            json=payload,
            headers={"Authorization": f"Bearer {self.api_key}"}
        )

        self.assertEqual(response.status_code, 200)
        self.assertTrue(response.json()["success"])

    def test_insufficient_funds(self):
        """Test payment declined due to insufficient funds"""
        payload = {
            "amount": 1000,
            "currency": "USD",
            "card": {
                "number": "4000000000009995",  # Declined card
                "exp_month": 12,
                "exp_year": 2025,
                "cvc": "123"
            }
        }

        response = requests.post(
            f"{self.base_url}/charges",
            json=payload,
            headers={"Authorization": f"Bearer {self.api_key}"}
        )

        self.assertEqual(response.status_code, 402)
        self.assertEqual(response.json()["error_code"], "insufficient_funds")

PCI DSS Compliance Testing

PCI DSS (Payment Card Industry Data Security Standard) compliance is mandatory for applications handling payment card data.

Key PCI DSS Requirements for Mobile Apps

1. Never Store Sensitive Authentication Data:

  • CVV/CVC codes must never be stored
  • Full magnetic stripe data prohibited
  • PIN blocks must not be retained

2. Encrypt Cardholder Data:

// iOS encryption example
import CryptoKit

func encryptCardData(_ cardNumber: String, publicKey: String) throws -> Data {
    let key = try P256.KeyAgreement.PublicKey(pemRepresentation: publicKey)
    let symmetricKey = SymmetricKey(size: .bits256)

    let sealedBox = try AES.GCM.seal(
        cardNumber.data(using: .utf8)!,
        using: symmetricKey
    )

    return sealedBox.combined!
}

3. Implement Strong Access Controls:

  • Role-based access to payment data
  • Multi-factor authentication for admin access
  • Audit logging of payment operations

PCI DSS Testing Checklist

  • No plaintext card data stored in databases
  • No sensitive data in logs or error messages
  • SSL/TLS encryption for data transmission (TLS 1.2+)
  • Tokenization implemented for card storage
  • Memory cleared after payment processing
  • No card data in crash reports or analytics
  • Secure coding practices followed
  • Regular security scanning performed
  • Penetration testing conducted annually
  • Documentation of security procedures maintained

Testing for Data Leakage

# Check for sensitive data in logs
grep -r "4[0-9]{15}" ./logs/  # Search for potential credit card numbers
grep -r "cvv\|cvc" ./logs/    # Search for CVV references

# Analyze app traffic with proxy
mitmproxy -p 8080  # Intercept HTTPS traffic to verify encryption

Sandbox and Test Environments

Effective use of sandbox environments is crucial for comprehensive payment testing without financial risk.

Best Practices for Sandbox Testing

1. Environment Isolation:

# Configuration management
environments:
  development:
    payment_gateway: "sandbox"
    api_key: "${SANDBOX_API_KEY}"
    endpoint: "https://sandbox.payment-gateway.com"

  staging:
    payment_gateway: "sandbox"
    api_key: "${STAGING_API_KEY}"
    endpoint: "https://sandbox.payment-gateway.com"

  production:
    payment_gateway: "production"
    api_key: "${PRODUCTION_API_KEY}"
    endpoint: "https://api.payment-gateway.com"

2. Feature Flags for Payment Testing:

// Feature flag implementation
const config = {
    useSandbox: process.env.NODE_ENV !== 'production',
    enableTestCards: process.env.ENABLE_TEST_CARDS === 'true',
    skipReceiptValidation: process.env.SKIP_RECEIPT_VALIDATION === 'true'
};

function getPaymentClient() {
    return config.useSandbox
        ? new SandboxPaymentClient(config.sandboxApiKey)
        : new ProductionPaymentClient(config.productionApiKey);
}

Sandbox Limitations to Consider

Common sandbox limitations:

  • Limited support for webhooks and callbacks
  • Reduced rate limits compared to production
  • Incomplete feature parity (some advanced features unavailable)
  • Data persistence may be temporary
  • 3D Secure simulation may be simplified

Workarounds:

  • Mock server for webhook testing
  • Rate limit testing in production-like environment
  • Document known sandbox limitations in test plans

Security Testing for Payment Flows

Security testing for payment systems requires specialized techniques beyond standard application security testing.

Man-in-the-Middle (MITM) Attack Testing

# Certificate pinning test
import ssl
import socket

def test_certificate_pinning(hostname, port, expected_cert_hash):
    """Verify app implements certificate pinning"""
    context = ssl.create_default_context()

    with socket.create_connection((hostname, port)) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            cert_der = ssock.getpeercert(binary_form=True)
            cert_hash = hashlib.sha256(cert_der).hexdigest()

            assert cert_hash == expected_cert_hash, \
                "Certificate hash mismatch - possible MITM attack"

Sensitive Data Exposure Testing

Test for data leakage in various locations:

# iOS Keychain inspection
security dump-keychain ~/Library/Keychains/login.keychain-db

# Android SharedPreferences check
adb shell run-as com.yourapp.package cat /data/data/com.yourapp.package/shared_prefs/preferences.xml

# Memory dump analysis
fridump -U com.yourapp.package -s
grep -r "4[0-9]{15}" dump/  # Search for card numbers in memory

Session Security Testing

Test cases for payment session security:

  • Session timeout after inactivity
  • Session invalidation after payment completion
  • Prevention of concurrent sessions
  • CSRF token validation
  • Replay attack prevention
// Session management test
describe('Payment Session Security', () => {
    it('should expire session after 15 minutes of inactivity', async () => {
        const session = await createPaymentSession();

        // Wait for timeout period
        await sleep(15 * 60 * 1000);

        // Attempt to use expired session
        const result = await processPayment(session.id);

        expect(result.error).toBe('session_expired');
    });

    it('should prevent replay attacks', async () => {
        const payment = await capturePaymentRequest();

        // Process payment
        await processPayment(payment);

        // Attempt to replay same payment
        const replayResult = await processPayment(payment);

        expect(replayResult.error).toBe('duplicate_transaction');
    });
});

Testing Payment Failure Scenarios

Thorough testing of failure scenarios is essential for robust payment systems.

Common Failure Scenarios

1. Network Failures:

// Simulate network timeout
@Test
fun testPaymentNetworkTimeout() {
    // Mock network layer to simulate timeout
    mockWebServer.enqueue(MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE))

    val result = paymentService.processPayment(testPaymentData)

    assertTrue(result is PaymentResult.NetworkError)
    assertEquals("Request timeout", result.message)
}

2. Insufficient Funds:

func testInsufficientFunds() {
    let testCard = TestCard(number: "4000000000009995") // Declined card

    let expectation = XCTestExpectation(description: "Payment declined")

    paymentProcessor.process(card: testCard, amount: 100.00) { result in
        switch result {
        case .failure(let error):
            XCTAssertEqual(error, .insufficientFunds)
            expectation.fulfill()
        case .success:
            XCTFail("Payment should have been declined")
        }
    }

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

3. Invalid Card Details:

Test ScenarioTest CardExpected Result
Invalid number4242424242424241Card validation error
Expired cardExp: 01/2020Expired card error
Invalid CVVCVV: 99Invalid CVV error
Invalid ZIPZIP: 00000ZIP mismatch error

Error Recovery Testing

// Test automatic retry logic
async function testPaymentRetry() {
    let attempts = 0;
    const maxRetries = 3;

    // Mock payment service to fail twice, succeed on third attempt
    paymentService.process = jest.fn()
        .mockRejectedValueOnce(new Error('Temporary failure'))
        .mockRejectedValueOnce(new Error('Temporary failure'))
        .mockResolvedValueOnce({ success: true, transactionId: '12345' });

    const result = await paymentWithRetry(paymentData, maxRetries);

    expect(paymentService.process).toHaveBeenCalledTimes(3);
    expect(result.success).toBe(true);
}

Subscription and Recurring Payment Testing

Subscription models require specialized testing for various lifecycle events.

Subscription Lifecycle Test Scenarios

1. New Subscription Creation:

func testSubscriptionCreation() {
    let subscription = Subscription(
        productId: "monthly_premium",
        period: .monthly,
        price: 9.99
    )

    subscriptionManager.subscribe(to: subscription) { result in
        XCTAssertTrue(result.isSuccess)
        XCTAssertNotNil(result.receiptData)
        XCTAssertEqual(result.expirationDate, expectedDate)
    }
}

2. Renewal Testing:

@Test
fun testAutoRenewal() = runTest {
    // Create subscription expiring soon
    val subscription = createTestSubscription(
        expiresAt = now().plusMinutes(5)
    )

    // Wait for auto-renewal window
    advanceTimeBy(6.minutes)

    // Verify subscription renewed
    val renewed = subscriptionRepository.getSubscription(subscription.id)
    assertTrue(renewed.isActive)
    assertEquals(now().plusMonths(1), renewed.expiresAt)
}

3. Subscription Upgrade/Downgrade:

ScenarioFrom PlanTo PlanExpected Behavior
UpgradeBasic ($9.99)Premium ($19.99)Immediate upgrade, prorated charge
DowngradePremium ($19.99)Basic ($9.99)Change at end of period
Cross-gradeMonthly ($9.99)Annual ($99.99)Prorated credit applied

Testing Billing Cycles

def test_billing_cycle():
    """Test subscription billing across multiple cycles"""
    subscription = create_subscription(
        user_id="test_user",
        plan="monthly",
        start_date=datetime(2025, 1, 1)
    )

    # Test first billing
    first_charge = process_billing_cycle(subscription, datetime(2025, 1, 1))
    assert first_charge.amount == 9.99
    assert first_charge.status == "succeeded"

    # Test second billing
    second_charge = process_billing_cycle(subscription, datetime(2025, 2, 1))
    assert second_charge.amount == 9.99
    assert second_charge.status == "succeeded"

    # Test failed payment
    set_payment_method_invalid(subscription)
    third_charge = process_billing_cycle(subscription, datetime(2025, 3, 1))
    assert third_charge.status == "failed"
    assert subscription.status == "past_due"

Grace Period and Retry Logic

describe('Subscription Grace Period', () => {
    it('should retry failed payments during grace period', async () => {
        const subscription = await createSubscription({
            gracePeriodDays: 7,
            retryAttempts: 3
        });

        // Simulate failed payment
        await simulateFailedPayment(subscription);

        // Verify subscription status
        expect(subscription.status).toBe('past_due');
        expect(subscription.gracePeriodEnds).toBe(addDays(now(), 7));

        // Simulate retry attempts
        for (let i = 1; i <= 3; i++) {
            await advanceTime(2, 'days');
            const retryResult = await retryPayment(subscription);

            if (i < 3) {
                expect(retryResult.attempt).toBe(i);
                expect(subscription.status).toBe('past_due');
            }
        }

        // Verify subscription cancelled after grace period
        await advanceTime(1, 'days');
        expect(subscription.status).toBe('cancelled');
    });
});

Refund and Cancellation Testing

Refund processing and cancellation workflows require careful testing to ensure correct financial reconciliation.

Refund Test Scenarios

class RefundTests(unittest.TestCase):
    def test_full_refund(self):
        """Test complete refund of payment"""
        # Create and process payment
        payment = create_payment(amount=100.00)
        process_payment(payment)

        # Process full refund
        refund = create_refund(
            payment_id=payment.id,
            amount=100.00,
            reason="customer_request"
        )

        self.assertEqual(refund.status, "succeeded")
        self.assertEqual(refund.amount, 100.00)
        self.assertEqual(payment.status, "refunded")

    def test_partial_refund(self):
        """Test partial refund of payment"""
        payment = create_payment(amount=100.00)
        process_payment(payment)

        # Process partial refund
        refund = create_refund(
            payment_id=payment.id,
            amount=50.00,
            reason="partial_return"
        )

        self.assertEqual(refund.status, "succeeded")
        self.assertEqual(refund.amount, 50.00)
        self.assertEqual(payment.amount_refunded, 50.00)
        self.assertEqual(payment.status, "partially_refunded")

    def test_refund_deadline(self):
        """Test refund cannot be processed after deadline"""
        # Create payment 6 months ago
        payment = create_payment(
            amount=100.00,
            created_at=datetime.now() - timedelta(days=180)
        )

        # Attempt refund
        with self.assertRaises(RefundError) as context:
            create_refund(payment_id=payment.id, amount=100.00)

        self.assertIn("refund_deadline_exceeded", str(context.exception))

Cancellation Testing

func testSubscriptionCancellation() {
    let subscription = createActiveSubscription()

    // Test immediate cancellation
    subscriptionManager.cancel(subscription, immediately: true) { result in
        XCTAssertTrue(result.isSuccess)
        XCTAssertEqual(subscription.status, .cancelled)
        XCTAssertNil(subscription.nextBillingDate)
        XCTAssertTrue(subscription.accessExpiresAt <= Date())
    }

    // Test cancellation at period end
    let subscription2 = createActiveSubscription()
    subscriptionManager.cancel(subscription2, immediately: false) { result in
        XCTAssertTrue(result.isSuccess)
        XCTAssertEqual(subscription2.status, .cancelledAtPeriodEnd)
        XCTAssertNotNil(subscription2.accessExpiresAt)
        XCTAssertTrue(subscription2.accessExpiresAt > Date())
    }
}

Multi-Currency and Localization Testing

Mobile apps serving global audiences must handle multiple currencies and localization correctly.

Currency Testing Scenarios

const currencyTestCases = [
    { currency: 'USD', amount: 100.00, formatted: '$100.00' },
    { currency: 'EUR', amount: 100.00, formatted: '100,00 €' },
    { currency: 'GBP', amount: 100.00, formatted: '£100.00' },
    { currency: 'JPY', amount: 10000, formatted: '¥10,000' },
    { currency: 'RUB', amount: 7500, formatted: '7 500 ₽' }
];

describe('Currency Formatting', () => {
    currencyTestCases.forEach(testCase => {
        it(`should format ${testCase.currency} correctly`, () => {
            const formatted = formatCurrency(
                testCase.amount,
                testCase.currency
            );
            expect(formatted).toBe(testCase.formatted);
        });
    });
});

Exchange Rate Testing

@Test
fun testCurrencyConversion() {
    val converter = CurrencyConverter()

    // Mock exchange rate service
    whenever(exchangeRateService.getRate("USD", "EUR"))
        .thenReturn(0.85)

    val result = converter.convert(
        amount = 100.0,
        from = "USD",
        to = "EUR"
    )

    assertEquals(85.0, result, 0.01)
}

@Test
fun testPriceConsistency() {
    // Ensure prices are consistent across currencies
    val usdPrice = 9.99
    val eurPrice = 8.49
    val gbpPrice = 7.99

    val currentRate = exchangeRateService.getRate("USD", "EUR")
    val convertedPrice = usdPrice * currentRate

    // Allow 5% variance for price rounding
    assertTrue(abs(convertedPrice - eurPrice) / eurPrice < 0.05)
}

Regional Payment Method Testing

Different regions support different payment methods:

RegionPayment MethodsTest Requirements
North AmericaCredit Cards, Apple Pay, Google Pay, PayPalTest all major card networks
EuropeSEPA, Credit Cards, Apple Pay, Google Pay, BancontactSEPA Direct Debit flow
AsiaAlipay, WeChat Pay, Credit Cards, Line PayQR code payment flow
Latin AmericaCredit Cards, Mercado Pago, PIX, OXXOCash payment vouchers

Automated Payment Testing and CI/CD

Integrating payment tests into CI/CD pipelines ensures continuous validation of payment functionality.

Automated Test Suite Structure

# .github/workflows/payment-tests.yml
name: Payment Integration Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  payment-tests:
    runs-on: ubuntu-latest

    env:
      STRIPE_TEST_KEY: ${{ secrets.STRIPE_TEST_KEY }}
      PAYPAL_SANDBOX_CLIENT: ${{ secrets.PAYPAL_SANDBOX_CLIENT }}

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Run payment unit tests
        run: npm run test:payment:unit

      - name: Run payment integration tests
        run: npm run test:payment:integration

      - name: Run payment security tests
        run: npm run test:payment:security

      - name: Generate payment test report
        if: always()
        run: npm run test:report

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: payment-test-results
          path: test-results/

Mock Payment Server for Testing

// mock-payment-server.js
const express = require('express');
const app = express();

app.post('/v1/charges', (req, res) => {
    const { card, amount } = req.body;

    // Simulate various card scenarios
    const testCards = {
        '4242424242424242': { success: true },
        '4000000000000002': { success: false, error: 'card_declined' },
        '4000000000009995': { success: false, error: 'insufficient_funds' },
        '4000000000000069': { success: false, error: 'expired_card' }
    };

    const result = testCards[card.number] || { success: true };

    if (result.success) {
        res.json({
            id: `ch_test_${Date.now()}`,
            amount: amount,
            status: 'succeeded',
            paid: true
        });
    } else {
        res.status(402).json({
            error: {
                type: 'card_error',
                code: result.error,
                message: getErrorMessage(result.error)
            }
        });
    }
});

app.listen(3001, () => {
    console.log('Mock payment server running on port 3001');
});

Performance Testing for Payment Flows

from locust import HttpUser, task, between

class PaymentLoadTest(HttpUser):
    wait_time = between(1, 3)

    @task(3)
    def create_payment_intent(self):
        """Test payment intent creation under load"""
        self.client.post("/v1/payment_intents", json={
            "amount": 1000,
            "currency": "usd",
            "payment_method_types": ["card"]
        }, headers={"Authorization": f"Bearer {self.api_key}"})

    @task(2)
    def confirm_payment(self):
        """Test payment confirmation under load"""
        # Create payment intent first
        response = self.client.post("/v1/payment_intents", json={
            "amount": 1000,
            "currency": "usd"
        }, headers={"Authorization": f"Bearer {self.api_key}"})

        if response.status_code == 200:
            intent_id = response.json()["id"]

            # Confirm payment
            self.client.post(f"/v1/payment_intents/{intent_id}/confirm",
                json={"payment_method": "pm_card_visa"},
                headers={"Authorization": f"Bearer {self.api_key}"})

    @task(1)
    def retrieve_payment(self):
        """Test payment retrieval performance"""
        self.client.get("/v1/charges/ch_test_12345",
            headers={"Authorization": f"Bearer {self.api_key}"})

Continuous Monitoring

// Monitoring payment health in production
const monitor = {
    async checkPaymentHealth() {
        const metrics = {
            successRate: await this.calculateSuccessRate(),
            avgProcessingTime: await this.getAvgProcessingTime(),
            failureReasons: await this.getFailureBreakdown()
        };

        // Alert if success rate drops below threshold
        if (metrics.successRate < 0.95) {
            this.sendAlert({
                severity: 'high',
                message: `Payment success rate dropped to ${metrics.successRate}`,
                metrics: metrics
            });
        }

        return metrics;
    },

    async calculateSuccessRate() {
        const last24h = new Date(Date.now() - 24 * 60 * 60 * 1000);
        const payments = await Payment.find({
            createdAt: { $gte: last24h }
        });

        const successful = payments.filter(p => p.status === 'succeeded').length;
        return successful / payments.length;
    }
};

// Run health check every 5 minutes
setInterval(() => monitor.checkPaymentHealth(), 5 * 60 * 1000);

Conclusion

Mobile payment testing requires a comprehensive approach covering platform-specific implementations, security compliance, sandbox environments, and various payment scenarios. Key takeaways for effective payment testing:

  1. Use sandbox environments extensively - Leverage test environments from Apple, Google, and payment gateways to validate functionality without financial risk
  2. Test all failure scenarios - Network issues, declined cards, timeouts, and edge cases must be thoroughly tested
  3. Prioritize security testing - PCI DSS compliance, data encryption, and secure transmission are critical
  4. Automate where possible - Integrate payment tests into CI/CD pipelines for continuous validation
  5. Test across platforms and regions - Different platforms, currencies, and regional payment methods require specific test coverage
  6. Monitor production metrics - Track payment success rates, processing times, and failure patterns in real-time

By following these guidelines and implementing comprehensive test coverage, QA engineers can ensure robust, secure, and reliable mobile payment systems that provide excellent user experiences while maintaining strict security and compliance standards.