Мобильные платежные системы революционизировали способ проведения транзакций на смартфонах и планшетах. От бесконтактных платежей с Apple Pay и Google Pay до встроенных покупок и моделей подписки, тестирование мобильных платежей требует специализированных знаний стандартов безопасности, платформо-специфичных реализаций и требований соответствия. Это всеобъемлющее руководство охватывает все, что нужно знать QA инженерам об эффективном тестировании мобильных платежных систем.

Введение в Задачи Тестирования Мобильных Платежей

Тестирование мобильных платежей представляет уникальные задачи, значительно отличающиеся от традиционного тестирования веб-платежей. Сложность проистекает из множества факторов:

Фрагментация Платформ: Разные операционные системы (iOS, Android) (как обсуждается в Cross-Platform Mobile Testing: Strategies for Multi-Device Success) (как обсуждается в Appium 2.0: New Architecture and Cloud Integration for Modern Mobile Testing) реализуют платежные системы по-разному, требуя платформо-специфичных подходов к тестированию.

Требования Безопасности: Платежные системы должны соответствовать PCI DSS (Стандарт Безопасности Данных Индустрии Платежных Карт), требуя строгого тестирования безопасности и мер защиты данных.

Ограничения Sandbox: Тестовые среды часто имеют ограниченную функциональность по сравнению с продакшеном, делая комплексное тестирование сложным.

Зависимости от Третьих Сторон: Платежные шлюзы, банки и провайдеры платформ вводят внешние зависимости, которые могут влиять на надежность тестирования.

Критичность Пользовательского Опыта: Потоки платежей должны быть плавными и интуитивными, так как трение в процессе платежа напрямую влияет на конверсию и доход.

Ключевые задачи включают:

  • Тестирование без раскрытия реальных платежных данных
  • Валидация шифрования и безопасной передачи данных
  • Симуляция различных сценариев платежей (успех, отказ, таймаут)
  • Тестирование на разных устройствах, версиях ОС и условиях сети
  • Обеспечение соответствия региональным регуляциям платежей
  • Валидация рабочих процессов возврата и отмены
  • Тестирование логики подписок и повторяющихся платежей

Тестирование и Интеграция Apple Pay

Интеграция Apple Pay требует специфических стратегий тестирования из-за ограничений экосистемы и модели безопасности.

Настройка Тестовой Среды Apple Pay

Apple предоставляет sandbox среды для тестирования транзакций Apple Pay:

// Включить режим sandbox в iOS приложении
#if DEBUG
    let configuration = PKPaymentAuthorizationConfiguration()
    configuration.merchantIdentifier = "merchant.com.yourapp.sandbox"
    configuration.supportedNetworks = [.visa, .masterCard, .amex]
    configuration.merchantCapabilities = .capability3DS
#else
    // Продакшн конфигурация
#endif

Конфигурация Тестовых Карт

Apple предоставляет тестовые карты через Sandbox среду:

Тип КартыНомер Тестовой КартыОжидаемое Поведение
Visa4111111111111111Успешный платеж
Mastercard5555555555554444Успешный платеж
Amex378282246310005Успешный платеж
Отклонена4000000000000002Отклоненная транзакция

Критические Тестовые Сценарии для Apple Pay

1. Поток Авторизации Платежа

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: "Тестовый Продукт", amount: NSDecimalNumber(string: "9.99"))
    request.paymentSummaryItems = [item]

    // Проверить представление контроллера авторизации
    let controller = PKPaymentAuthorizationController(paymentRequest: request)
    controller.delegate = self
    controller.present { presented in
        XCTAssertTrue(presented, "Контроллер платежа должен быть представлен")
    }
}

2. Тестирование Биометрической Аутентификации

Тестировать Apple Pay с Face ID/Touch ID:

  • Валидная биометрическая аутентификация
  • Неудачные попытки аутентификации
  • Откат к вводу пароля
  • Отключенные биометрические настройки

3. Тестирование Совместимости Устройств

Проверить поддержку Apple Pay на:

  • Моделях iPhone (iPhone 6 и новее)
  • Интеграции Apple Watch
  • Моделях iPad с Touch ID/Face ID
  • Mac с Touch ID

Чек-лист Валидации Apple Pay

  • Sandbox среда настроена корректно
  • Тестовый идентификатор мерчанта активен
  • Лист платежа отображает корректную сумму и имя мерчанта
  • Поддерживаемые сети карт настроены правильно
  • Аутентификация 3D Secure работает
  • Квитанция транзакции генерируется корректно
  • Обработка ошибок для отклоненных платежей
  • Сценарии таймаута сети обработаны
  • Валидация адреса доставки (если применимо)
  • Сбор контактной информации работает

Стратегии Тестирования Google Pay

Google Pay предлагает более гибкую тестовую среду по сравнению с Apple Pay, с обширными возможностями sandbox.

Настройка Тестовой Среды Google Pay

// Конфигурация Google Pay в Android
private fun createPaymentsClient(): PaymentsClient {
    val walletOptions = Wallet.WalletOptions.Builder()
        .setEnvironment(WalletConstants.ENVIRONMENT_TEST) // Использовать тестовую среду
        .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": "Тестовый Мерчант"
            },
            "transactionInfo": {
                "totalPriceStatus": "FINAL",
                "totalPrice": "12.34",
                "currencyCode": "USD"
            }
        }
        """.trimIndent()
    )
    return request
}

Тестовые Карты Google Pay

Google предоставляет комплексные тестовые карты для различных сценариев:

СценарийТестовая КартаCVVZIP
Успех411111111111111112312345
Отклонена - Недостаточно Средств400000000000999512312345
Отклонена - Утерянная Карта400000000000998712312345
Аутентификация 3D Secure400000000000322012312345

Ключевые Тестовые Сценарии Google Pay

1. Доступность Метода Платежа

@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 должен быть доступен", task.isSuccessful)
        }
}

2. Валидация Токена

Проверить структуру и шифрование платежного токена:

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

    // Проверить обязательные поля
    assertTrue(jsonToken.has("signature"))
    assertTrue(jsonToken.has("protocolVersion"))
    assertTrue(jsonToken.has("signedMessage"))

    // Проверить валидность подписи
    val signedMessage = jsonToken.getString("signedMessage")
    assertNotNull(signedMessage)
    assertFalse(signedMessage.isEmpty())
}

Тестирование Встроенных Покупок (IAP) для iOS и Android

Встроенные покупки требуют платформо-специфичных подходов тестирования для расходуемых, нерасходуемых, подписок и автообновляемого контента.

Тестирование Встроенных Покупок iOS

Конфигурация Тестера Sandbox:

  1. Создать тестовые аккаунты sandbox в App Store Connect
  2. Настроить разные типы аккаунтов (новые пользователи, существующие подписчики)
  3. Тестировать с различными регионами аккаунтов для локализации
// Тестирование StoreKit в iOS
import StoreKit

class IAPManager: NSObject, SKPaymentTransactionObserver {
    func purchaseProduct(productId: String) {
        guard SKPaymentQueue.canMakePayments() else {
            print("Пользователь не может совершать платежи")
            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:
                // Проверить квитанцию и разблокировать контент
                completeTransaction(transaction)
            case .failed:
                // Обработать сбой
                failedTransaction(transaction)
            case .restored:
                // Восстановить предыдущую покупку
                restoreTransaction(transaction)
            case .deferred:
                // Платеж ожидает одобрения
                print("Платеж отложен")
            case .purchasing:
                print("Покупка...")
            @unknown default:
                break
            }
        }
    }
}

Тестовые Кейсы IAP для iOS:

  • Получение продуктов из App Store
  • Завершение потока покупки
  • Валидация квитанции (локальная и серверная)
  • Восстановление транзакций
  • Прерванные транзакции (завершение приложения во время покупки)
  • Поддержка Семейного Доступа
  • Промо-предложения и вступительные цены

Тестирование Встроенной Оплаты Android

Библиотека Оплаты Google Play предоставляет комплексные инструменты тестирования:

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) {
                    // Запросить доступные продукты
                    queryProducts()
                }
            }

            override fun onBillingServiceDisconnected() {
                // Повторить попытку подключения
            }
        })
    }

    override fun onPurchasesUpdated(result: BillingResult, purchases: List<Purchase>?) {
        when (result.responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                purchases?.forEach { purchase ->
                    if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
                        // Подтвердить покупку
                        acknowledgePurchase(purchase)
                    }
                }
            }
            BillingClient.BillingResponseCode.USER_CANCELED -> {
                // Обработать отмену пользователем
            }
            else -> {
                // Обработать другие ошибки
            }
        }
    }
}

Тестовые ID Покупок Android:

Google предоставляет зарезервированные ID продуктов для тестирования:

  • android.test.purchased - Всегда успешная покупка
  • android (как обсуждается в [Detox: Grey-Box Testing for React Native Applications](/blog/detox-react-native-grey-box)).test.canceled - Симулирует отмененную покупку
  • android.test.refunded - Симулирует возвращенную покупку
  • android.test.item_unavailable - Симулирует недоступный продукт

Тестирование Интеграции Платежного Шлюза

Интеграция платежного шлюза требует тестирования коммуникации между вашим приложением, бэкенд-сервером и платежным процессором.

Чек-лист Тестирования Платежного Шлюза

Точки Интеграции для Тестирования:

// Пример обработки платежей на бэкенде (Node.js)
const stripe = require('stripe')(process.env.STRIPE_TEST_KEY);

async function processPayment(paymentData) {
    try {
        // Создать намерение платежа
        const paymentIntent = await stripe.paymentIntents.create({
            amount: paymentData.amount,
            currency: 'usd',
            payment_method_types: ['card'],
            metadata: {
                orderId: paymentData.orderId,
                userId: paymentData.userId
            }
        });

        // Подтвердить платеж
        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
        };
    }
}

Распространенные Платежные Шлюзы и Тестовые Среды

ШлюзНастройка Тестового РежимаПредоставленные Тестовые Карты
StripeИспользовать тестовые API ключи с префиксом sk_test_Комплексный набор тестовых карт
PayPalSandbox среда PayPalТребуются sandbox аккаунты
BraintreeSandbox ID мерчантаПредоставлены тестовые номера карт
SquareSandbox токен доступаТестовые карты для различных сценариев
AdyenТестовый аккаунт мерчантаТестовые номера карт по регионам

Сценарии Тестирования API Интеграции

# Пример интеграционного теста на Python
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):
        """Тестировать успешную обработку платежа"""
        payload = {
            "amount": 1000,  # Сумма в центах
            "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):
        """Тестировать отклонение платежа из-за недостатка средств"""
        payload = {
            "amount": 1000,
            "currency": "USD",
            "card": {
                "number": "4000000000009995",  # Отклоненная карта
                "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

Соответствие PCI DSS (Стандарт Безопасности Данных Индустрии Платежных Карт) обязательно для приложений, обрабатывающих данные платежных карт.

Ключевые Требования PCI DSS для Мобильных Приложений

1. Никогда не Хранить Чувствительные Данные Аутентификации:

  • Коды CVV/CVC никогда не должны храниться
  • Полные данные магнитной полосы запрещены
  • Блоки PIN не должны сохраняться

2. Шифровать Данные Держателя Карты:

// Пример шифрования в iOS
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. Реализовать Строгие Контроли Доступа:

  • Ролевой доступ к платежным данным
  • Многофакторная аутентификация для админ доступа
  • Аудит логирование платежных операций

Чек-лист Тестирования PCI DSS

  • Нет данных карты в открытом виде в базах данных
  • Нет чувствительных данных в логах или сообщениях об ошибках
  • SSL/TLS шифрование для передачи данных (TLS 1.2+)
  • Токенизация реализована для хранения карт
  • Память очищена после обработки платежа
  • Нет данных карт в отчетах о сбоях или аналитике
  • Практики безопасного кодирования соблюдены
  • Регулярное сканирование безопасности выполняется
  • Тестирование на проникновение проводится ежегодно
  • Документация процедур безопасности поддерживается

Тестирование Утечки Данных

# Проверить чувствительные данные в логах
grep -r "4[0-9]{15}" ./logs/  # Искать возможные номера кредитных карт
grep -r "cvv\|cvc" ./logs/    # Искать упоминания CVV

# Анализировать трафик приложения с прокси
mitmproxy -p 8080  # Перехватывать HTTPS трафик для проверки шифрования

Sandbox и Тестовые Среды

Эффективное использование sandbox сред критично для комплексного тестирования платежей без финансового риска.

Лучшие Практики для Тестирования в Sandbox

1. Изоляция Среды:

# Управление конфигурацией
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. Флаги Функций для Тестирования Платежей:

// Реализация флага функции
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 для Рассмотрения

Распространенные ограничения sandbox:

  • Ограниченная поддержка вебхуков и коллбэков
  • Сниженные лимиты частоты по сравнению с продакшеном
  • Неполное соответствие функций (некоторые продвинутые функции недоступны)
  • Сохранение данных может быть временным
  • Симуляция 3D Secure может быть упрощена

Обходные пути:

  • Mock сервер для тестирования вебхуков
  • Тестирование лимитов частоты в продакшен-подобной среде
  • Документировать известные ограничения sandbox в планах тестирования

Тестирование Безопасности для Платежных Потоков

Тестирование безопасности для платежных систем требует специализированных техник помимо стандартного тестирования безопасности приложений.

Тестирование Атак Man-in-the-Middle (MITM)

# Тест certificate pinning
import ssl
import socket

def test_certificate_pinning(hostname, port, expected_cert_hash):
    """Проверить, что приложение реализует 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, \
                "Хеш сертификата не совпадает - возможная MITM атака"

Тестирование Утечки Чувствительных Данных

Тестировать утечку данных в различных местах:

# Инспекция iOS Keychain
security dump-keychain ~/Library/Keychains/login.keychain-db

# Проверка SharedPreferences в Android
adb shell run-as com.yourapp.package cat /data/data/com.yourapp.package/shared_prefs/preferences.xml

# Анализ дампа памяти
fridump -U com.yourapp.package -s
grep -r "4[0-9]{15}" dump/  # Искать номера карт в памяти

Тестирование Безопасности Сессии

Тестовые кейсы для безопасности платежной сессии:

  • Таймаут сессии после бездействия
  • Инвалидация сессии после завершения платежа
  • Предотвращение одновременных сессий
  • Валидация CSRF токена
  • Предотвращение replay атак
// Тест управления сессией
describe('Безопасность Платежной Сессии', () => {
    it('должна истекать сессия после 15 минут бездействия', async () => {
        const session = await createPaymentSession();

        // Ждать периода таймаута
        await sleep(15 * 60 * 1000);

        // Попытаться использовать истекшую сессию
        const result = await processPayment(session.id);

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

    it('должна предотвращать replay атаки', async () => {
        const payment = await capturePaymentRequest();

        // Обработать платеж
        await processPayment(payment);

        // Попытаться replay того же платежа
        const replayResult = await processPayment(payment);

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

Тестирование Сценариев Отказа Платежа

Тщательное тестирование сценариев отказа необходимо для надежных платежных систем.

Распространенные Сценарии Отказа

1. Сетевые Отказы:

// Симулировать сетевой таймаут
@Test
fun testPaymentNetworkTimeout() {
    // Замокать сетевой слой для симуляции таймаута
    mockWebServer.enqueue(MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE))

    val result = paymentService.processPayment(testPaymentData)

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

2. Недостаточно Средств:

func testInsufficientFunds() {
    let testCard = TestCard(number: "4000000000009995") // Отклоненная карта

    let expectation = XCTestExpectation(description: "Платеж отклонен")

    paymentProcessor.process(card: testCard, amount: 100.00) { result in
        switch result {
        case .failure(let error):
            XCTAssertEqual(error, .insufficientFunds)
            expectation.fulfill()
        case .success:
            XCTFail("Платеж должен был быть отклонен")
        }
    }

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

3. Неверные Данные Карты:

Тестовый СценарийТестовая КартаОжидаемый Результат
Неверный номер4242424242424241Ошибка валидации карты
Истекшая картаExp: 01/2020Ошибка истекшей карты
Неверный CVVCVV: 99Ошибка неверного CVV
Неверный ZIPZIP: 00000Ошибка несовпадения ZIP

Тестирование Восстановления от Ошибок

// Тестировать логику автоматического повтора
async function testPaymentRetry() {
    let attempts = 0;
    const maxRetries = 3;

    // Замокать платежный сервис для двух отказов, успех на третьей попытке
    paymentService.process = jest.fn()
        .mockRejectedValueOnce(new Error('Временный отказ'))
        .mockRejectedValueOnce(new Error('Временный отказ'))
        .mockResolvedValueOnce({ success: true, transactionId: '12345' });

    const result = await paymentWithRetry(paymentData, maxRetries);

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

Тестирование Подписок и Повторяющихся Платежей

Модели подписок требуют специализированного тестирования для различных событий жизненного цикла.

Сценарии Тестирования Жизненного Цикла Подписки

1. Создание Новой Подписки:

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. Тестирование Продления:

@Test
fun testAutoRenewal() = runTest {
    // Создать подписку, истекающую скоро
    val subscription = createTestSubscription(
        expiresAt = now().plusMinutes(5)
    )

    // Ждать окна автопродления
    advanceTimeBy(6.minutes)

    // Проверить продленную подписку
    val renewed = subscriptionRepository.getSubscription(subscription.id)
    assertTrue(renewed.isActive)
    assertEquals(now().plusMonths(1), renewed.expiresAt)
}

3. Повышение/Понижение Подписки:

СценарийС ПланаНа ПланОжидаемое Поведение
ПовышениеБазовый ($9.99)Премиум ($19.99)Немедленное повышение, пропорциональная оплата
ПонижениеПремиум ($19.99)Базовый ($9.99)Изменение в конце периода
ПереключениеМесячный ($9.99)Годовой ($99.99)Применен пропорциональный кредит

Тестирование Циклов Оплаты

def test_billing_cycle():
    """Тестировать оплату подписки через несколько циклов"""
    subscription = create_subscription(
        user_id="test_user",
        plan="monthly",
        start_date=datetime(2025, 1, 1)
    )

    # Тестировать первую оплату
    first_charge = process_billing_cycle(subscription, datetime(2025, 1, 1))
    assert first_charge.amount == 9.99
    assert first_charge.status == "succeeded"

    # Тестировать вторую оплату
    second_charge = process_billing_cycle(subscription, datetime(2025, 2, 1))
    assert second_charge.amount == 9.99
    assert second_charge.status == "succeeded"

    # Тестировать неудачный платеж
    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"

Льготный Период и Логика Повтора

describe('Льготный Период Подписки', () => {
    it('должен повторять неудачные платежи во время льготного периода', async () => {
        const subscription = await createSubscription({
            gracePeriodDays: 7,
            retryAttempts: 3
        });

        // Симулировать неудачный платеж
        await simulateFailedPayment(subscription);

        // Проверить статус подписки
        expect(subscription.status).toBe('past_due');
        expect(subscription.gracePeriodEnds).toBe(addDays(now(), 7));

        // Симулировать попытки повтора
        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');
            }
        }

        // Проверить отмену подписки после льготного периода
        await advanceTime(1, 'days');
        expect(subscription.status).toBe('cancelled');
    });
});

Тестирование Возвратов и Отмены

Обработка возвратов и рабочие процессы отмены требуют тщательного тестирования для обеспечения корректной финансовой сверки.

Сценарии Тестирования Возврата

class RefundTests(unittest.TestCase):
    def test_full_refund(self):
        """Тестировать полный возврат платежа"""
        # Создать и обработать платеж
        payment = create_payment(amount=100.00)
        process_payment(payment)

        # Обработать полный возврат
        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):
        """Тестировать частичный возврат платежа"""
        payment = create_payment(amount=100.00)
        process_payment(payment)

        # Обработать частичный возврат
        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):
        """Тестировать что возврат не может быть обработан после дедлайна"""
        # Создать платеж 6 месяцев назад
        payment = create_payment(
            amount=100.00,
            created_at=datetime.now() - timedelta(days=180)
        )

        # Попытаться вернуть
        with self.assertRaises(RefundError) as context:
            create_refund(payment_id=payment.id, amount=100.00)

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

Тестирование Отмены

func testSubscriptionCancellation() {
    let subscription = createActiveSubscription()

    // Тестировать немедленную отмену
    subscriptionManager.cancel(subscription, immediately: true) { result in
        XCTAssertTrue(result.isSuccess)
        XCTAssertEqual(subscription.status, .cancelled)
        XCTAssertNil(subscription.nextBillingDate)
        XCTAssertTrue(subscription.accessExpiresAt <= Date())
    }

    // Тестировать отмену в конце периода
    let subscription2 = createActiveSubscription()
    subscriptionManager.cancel(subscription2, immediately: false) { result in
        XCTAssertTrue(result.isSuccess)
        XCTAssertEqual(subscription2.status, .cancelledAtPeriodEnd)
        XCTAssertNotNil(subscription2.accessExpiresAt)
        XCTAssertTrue(subscription2.accessExpiresAt > Date())
    }
}

Тестирование Множественных Валют и Локализации

Мобильные приложения, обслуживающие глобальную аудиторию, должны корректно обрабатывать множественные валюты и локализацию.

Сценарии Тестирования Валют

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('Форматирование Валют', () => {
    currencyTestCases.forEach(testCase => {
        it(`должно форматировать ${testCase.currency} корректно`, () => {
            const formatted = formatCurrency(
                testCase.amount,
                testCase.currency
            );
            expect(formatted).toBe(testCase.formatted);
        });
    });
});

Тестирование Обменного Курса

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

    // Замокать сервис обменного курса
    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() {
    // Обеспечить согласованность цен между валютами
    val usdPrice = 9.99
    val eurPrice = 8.49
    val gbpPrice = 7.99

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

    // Допускать 5% отклонение для округления цен
    assertTrue(abs(convertedPrice - eurPrice) / eurPrice < 0.05)
}

Тестирование Региональных Методов Платежа

Разные регионы поддерживают разные методы платежа:

РегионМетоды ПлатежаТребования Тестирования
Северная АмерикаКредитные карты, Apple Pay, Google Pay, PayPalТестировать все основные сети
ЕвропаSEPA, Кредитные карты, Apple Pay, Google Pay, BancontactПоток прямого дебета SEPA
АзияAlipay, WeChat Pay, Кредитные карты, Line PayПоток платежа QR кодом
Латинская АмерикаКредитные карты, Mercado Pago, PIX, OXXOВаучеры наличной оплаты

Автоматизированное Тестирование Платежей и CI/CD

Интеграция платежных тестов в CI/CD пайплайны обеспечивает непрерывную валидацию платежной функциональности.

Структура Автоматизированной Тестовой Сюиты

# .github/workflows/payment-tests.yml
name: Интеграционные Тесты Платежей

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: Настроить Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Установить зависимости
        run: npm ci

      - name: Запустить юнит тесты платежей
        run: npm run test:payment:unit

      - name: Запустить интеграционные тесты платежей
        run: npm run test:payment:integration

      - name: Запустить тесты безопасности платежей
        run: npm run test:payment:security

      - name: Сгенерировать отчет о тестах платежей
        if: always()
        run: npm run test:report

      - name: Загрузить результаты тестов
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: payment-test-results
          path: test-results/

Mock Платежный Сервер для Тестирования

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

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

    // Симулировать различные сценарии карт
    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 платежный сервер запущен на порту 3001');
});

Тестирование Производительности для Платежных Потоков

from locust import HttpUser, task, between

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

    @task(3)
    def create_payment_intent(self):
        """Тестировать создание намерения платежа под нагрузкой"""
        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):
        """Тестировать подтверждение платежа под нагрузкой"""
        # Сначала создать намерение платежа
        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"]

            # Подтвердить платеж
            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):
        """Тестировать производительность получения платежа"""
        self.client.get("/v1/charges/ch_test_12345",
            headers={"Authorization": f"Bearer {self.api_key}"})

Непрерывный Мониторинг

// Мониторинг здоровья платежей в продакшене
const monitor = {
    async checkPaymentHealth() {
        const metrics = {
            successRate: await this.calculateSuccessRate(),
            avgProcessingTime: await this.getAvgProcessingTime(),
            failureReasons: await this.getFailureBreakdown()
        };

        // Оповестить если уровень успеха падает ниже порога
        if (metrics.successRate < 0.95) {
            this.sendAlert({
                severity: 'high',
                message: `Уровень успеха платежей упал до ${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;
    }
};

// Запускать проверку здоровья каждые 5 минут
setInterval(() => monitor.checkPaymentHealth(), 5 * 60 * 1000);

Заключение

Тестирование мобильных платежей требует комплексного подхода, охватывающего платформо-специфичные реализации, соответствие безопасности, sandbox среды и различные платежные сценарии. Ключевые выводы для эффективного тестирования платежей:

  1. Интенсивно использовать sandbox среды - Использовать тестовые среды от Apple, Google и платежных шлюзов для валидации функциональности без финансового риска
  2. Тестировать все сценарии отказа - Сетевые проблемы, отклоненные карты, таймауты и граничные случаи должны быть тщательно протестированы
  3. Приоритизировать тестирование безопасности - Соответствие PCI DSS, шифрование данных и безопасная передача критичны
  4. Автоматизировать где возможно - Интегрировать платежные тесты в CI/CD пайплайны для непрерывной валидации
  5. Тестировать кросс-платформенно и кросс-регионально - Разные платформы, валюты и региональные методы платежа требуют специфического тестового покрытия
  6. Мониторить продакшн метрики - Отслеживать уровни успеха платежей, время обработки и паттерны отказов в реальном времени

Следуя этим руководствам и реализуя комплексное тестовое покрытие, QA инженеры могут обеспечить надежные, безопасные и стабильные мобильные платежные системы, предоставляющие отличный пользовательский опыт при поддержании строгих стандартов безопасности и соответствия.