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 Type | Test Card Number | Expected Behavior |
---|---|---|
Visa | 4111111111111111 | Successful payment |
Mastercard | 5555555555554444 | Successful payment |
Amex | 378282246310005 | Successful payment |
Declined | 4000000000000002 | Declined 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:
Scenario | Test Card | CVV | ZIP |
---|---|---|---|
Success | 4111111111111111 | 123 | 12345 |
Declined - Insufficient Funds | 4000000000009995 | 123 | 12345 |
Declined - Lost Card | 4000000000009987 | 123 | 12345 |
3D Secure Authentication | 4000000000003220 | 123 | 12345 |
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:
- Create sandbox test accounts in App Store Connect
- Configure different account types (new users, existing subscribers)
- 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 purchaseandroid.test.canceled
- Simulates cancelled purchaseandroid.test.refunded
- Simulates refunded purchaseandroid.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
Gateway | Test Mode Setup | Test Cards Provided |
---|---|---|
Stripe | Use test API keys with sk_test_ prefix | Comprehensive test card suite |
PayPal | PayPal Sandbox environment | Sandbox accounts required |
Braintree | Sandbox merchant ID | Test card numbers provided |
Square | Sandbox access token | Test cards for various scenarios |
Adyen | Test merchant account | Test 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 Scenario | Test Card | Expected Result |
---|---|---|
Invalid number | 4242424242424241 | Card validation error |
Expired card | Exp: 01/2020 | Expired card error |
Invalid CVV | CVV: 99 | Invalid CVV error |
Invalid ZIP | ZIP: 00000 | ZIP 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:
Scenario | From Plan | To Plan | Expected Behavior |
---|---|---|---|
Upgrade | Basic ($9.99) | Premium ($19.99) | Immediate upgrade, prorated charge |
Downgrade | Premium ($19.99) | Basic ($9.99) | Change at end of period |
Cross-grade | Monthly ($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:
Region | Payment Methods | Test Requirements |
---|---|---|
North America | Credit Cards, Apple Pay, Google Pay, PayPal | Test all major card networks |
Europe | SEPA, Credit Cards, Apple Pay, Google Pay, Bancontact | SEPA Direct Debit flow |
Asia | Alipay, WeChat Pay, Credit Cards, Line Pay | QR code payment flow |
Latin America | Credit Cards, Mercado Pago, PIX, OXXO | Cash 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:
- Use sandbox environments extensively - Leverage test environments from Apple, Google, and payment gateways to validate functionality without financial risk
- Test all failure scenarios - Network issues, declined cards, timeouts, and edge cases must be thoroughly tested
- Prioritize security testing - PCI DSS compliance, data encryption, and secure transmission are critical
- Automate where possible - Integrate payment tests into CI/CD pipelines for continuous validation
- Test across platforms and regions - Different platforms, currencies, and regional payment methods require specific test coverage
- 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.