Testing PayPal and Braintree Payment Integrations

Testing PayPal and Braintree Payment Integrations

PayPal and Braintree both provide sandbox environments that mirror their production APIs. PayPal Sandbox uses test buyer/seller accounts. Braintree Sandbox provides test nonce values for different payment scenarios. Both support webhook testing for order completion and payment events. This guide covers setup, test credentials, automation patterns, and common gotchas.

Key Takeaways

PayPal Sandbox requires two sandbox accounts. You need a sandbox business account (the seller) and one or more sandbox personal accounts (buyers). Create both in the PayPal Developer Portal before writing any tests.

Braintree Sandbox nonces replace card numbers. Unlike Stripe's test cards, Braintree uses "payment method nonces" as test fixtures. fake-valid-nonce for success, fake-processor-declined-nonce for decline. Never use real card data in sandbox.

PayPal IPN/webhooks require a public URL. PayPal can't reach localhost. Use ngrok or a similar tunneling tool for local webhook testing, or mock the webhook payload in unit tests.

Braintree Drop-in UI is harder to automate. The Drop-in UI is rendered inside an iframe. Playwright and Cypress can interact with it, but selector stability varies between Braintree SDK versions — pin your SDK version in tests.

Both sandboxes have rate limits. Don't create hundreds of sandbox transactions in rapid succession in CI — add delays between tests or use mocking for high-volume test suites.

PayPal Sandbox Setup

Create sandbox accounts

  1. Go to developer.paypal.com
  2. Log in with your PayPal account
  3. Navigate to Sandbox > Accounts
  4. You'll see pre-created sandbox business and personal accounts
  5. Click on any account to get the credentials (email, password, client ID, secret)

For API calls, you need:

  • Sandbox App with Client ID and Secret (from Apps & CredentialsSandbox)
  • Sandbox Buyer Account email and password (for browser-based payment flows)

Get an access token

PayPal uses OAuth 2.0. Get a token before making API calls:

import requests
import base64

PAYPAL_CLIENT_ID = "your_sandbox_client_id"
PAYPAL_CLIENT_SECRET = "your_sandbox_client_secret"
PAYPAL_BASE_URL = "https://api-m.sandbox.paypal.com"


def get_paypal_token():
    credentials = base64.b64encode(
        f"{PAYPAL_CLIENT_ID}:{PAYPAL_CLIENT_SECRET}".encode()
    ).decode()
    
    response = requests.post(
        f"{PAYPAL_BASE_URL}/v1/oauth2/token",
        headers={
            "Authorization": f"Basic {credentials}",
            "Content-Type": "application/x-www-form-urlencoded",
        },
        data="grant_type=client_credentials",
    )
    response.raise_for_status()
    return response.json()["access_token"]

Testing the PayPal Orders API

# test_paypal_orders.py
import requests
import pytest


@pytest.fixture
def paypal_token():
    return get_paypal_token()


def test_create_paypal_order(paypal_token):
    response = requests.post(
        f"{PAYPAL_BASE_URL}/v2/checkout/orders",
        headers={
            "Authorization": f"Bearer {paypal_token}",
            "Content-Type": "application/json",
        },
        json={
            "intent": "CAPTURE",
            "purchase_units": [{
                "amount": {
                    "currency_code": "USD",
                    "value": "29.99",
                },
                "description": "Test Product",
            }],
        },
    )
    assert response.status_code == 201
    order = response.json()
    assert order["status"] == "CREATED"
    assert order["id"] is not None
    
    # Verify approval URL is present
    links = {link["rel"]: link["href"] for link in order["links"]}
    assert "approve" in links
    return order["id"]


def test_capture_approved_order(paypal_token):
    # Create order first
    order_id = test_create_paypal_order(paypal_token)
    
    # In real flow, the buyer would approve via PayPal UI
    # For automated testing, use the Orders API to simulate approval
    # (Note: full E2E requires browser automation with sandbox buyer account)
    
    # Capture the order (after approval)
    response = requests.post(
        f"{PAYPAL_BASE_URL}/v2/checkout/orders/{order_id}/capture",
        headers={
            "Authorization": f"Bearer {paypal_token}",
            "Content-Type": "application/json",
        },
    )
    # Note: This returns 422 if order is not approved yet
    # Use browser automation to complete the approval flow
    assert response.status_code in (201, 422)

PayPal Browser Automation with Playwright

For full E2E tests including the PayPal approval flow:

# test_paypal_e2e.py
from playwright.sync_api import Page
import pytest

SANDBOX_BUYER_EMAIL = "your-sandbox-buyer@personal.example.com"
SANDBOX_BUYER_PASSWORD = "your-sandbox-buyer-password"


def test_paypal_checkout_flow(page: Page):
    # Start checkout on your app
    page.goto("https://your-app.com/checkout")
    
    # Click PayPal button
    page.click("[data-testid='paypal-button']")
    
    # PayPal popup or redirect
    # If using popup:
    with page.expect_popup() as popup_info:
        page.click(".paypal-button")
    paypal_page = popup_info.value
    
    # Login with sandbox buyer account
    paypal_page.fill("#email", SANDBOX_BUYER_EMAIL)
    paypal_page.click("#btnNext")
    paypal_page.fill("#password", SANDBOX_BUYER_PASSWORD)
    paypal_page.click("#btnLogin")
    
    # Confirm payment
    paypal_page.wait_for_selector("#payment-submit-btn", timeout=10000)
    paypal_page.click("#payment-submit-btn")
    
    # Back on your app, check for success
    page.wait_for_url("**/success**")
    assert page.locator("[data-testid='order-confirmed']").is_visible()

Braintree Sandbox Setup

Install SDK

pip install braintree

Configure sandbox credentials

import braintree

gateway = braintree.BraintreeGateway(
    braintree.Configuration(
        environment=braintree.Environment.Sandbox,
        merchant_id="your_sandbox_merchant_id",
        public_key="your_sandbox_public_key",
        private_key="your_sandbox_private_key",
    )
)

Get credentials from the Braintree Sandbox Dashboard.

Braintree Test Nonces

Braintree uses payment method nonces as test fixtures. These replace card numbers for server-side tests:

Nonce Scenario
fake-valid-nonce Payment succeeds
fake-processor-declined-nonce Processor decline
fake-luhn-invalid-nonce Invalid card number
fake-consumed-nonce Nonce already used
fake-valid-visa-nonce Visa success
fake-valid-amex-nonce Amex success
fake-three-d-secure-visa-full-authentication-nonce 3DS authenticated
fake-paypal-one-time-nonce PayPal payment via Braintree
# test_braintree.py
import braintree
import pytest


@pytest.fixture
def gateway():
    return braintree.BraintreeGateway(
        braintree.Configuration(
            environment=braintree.Environment.Sandbox,
            merchant_id="your_merchant_id",
            public_key="your_public_key",
            private_key="your_private_key",
        )
    )


def test_successful_transaction(gateway):
    result = gateway.transaction.sale({
        "amount": "29.99",
        "payment_method_nonce": braintree.Test.Nonces.Transactable,
        "options": {
            "submit_for_settlement": True
        }
    })
    
    assert result.is_success
    assert result.transaction.status == braintree.Transaction.Status.SubmittedForSettlement
    assert result.transaction.amount == braintree.Decimal("29.99")


def test_declined_transaction(gateway):
    result = gateway.transaction.sale({
        "amount": "2000.00",  # Processor decline amount in Braintree sandbox
        "payment_method_nonce": braintree.Test.Nonces.ProcessorDeclined,
        "options": {
            "submit_for_settlement": True
        }
    })
    
    assert not result.is_success
    assert result.transaction.status == braintree.Transaction.Status.ProcessorDeclined


def test_client_token_generation(gateway):
    """Test that client tokens are generated for the frontend."""
    client_token = gateway.client_token.generate()
    assert client_token is not None
    assert len(client_token) > 0


def test_refund_transaction(gateway):
    # Create a settled transaction first
    result = gateway.transaction.sale({
        "amount": "50.00",
        "payment_method_nonce": braintree.Test.Nonces.Transactable,
        "options": {"submit_for_settlement": True}
    })
    assert result.is_success
    
    # Settle the transaction (sandbox only — settles immediately in test)
    transaction_id = result.transaction.id
    
    # Refund
    refund_result = gateway.transaction.refund(transaction_id, "25.00")
    assert refund_result.is_success
    assert refund_result.transaction.type == braintree.Transaction.Type.Credit
    assert refund_result.transaction.amount == braintree.Decimal("25.00")

Testing Braintree Drop-in UI with Playwright

def test_braintree_dropin_payment(page: Page):
    page.goto("https://your-app.com/checkout")
    
    # Wait for Braintree Drop-in to load
    page.wait_for_selector('iframe[src*="assets.braintreegateway.com"]')
    
    # Interact with the card number field inside the iframe
    card_frame = page.frame_locator(
        'iframe[src*="assets.braintreegateway.com"][title*="Card Number"]'
    )
    card_frame.locator('[placeholder="•••• •••• •••• ••••"]').fill("4111 1111 1111 1111")
    
    expiry_frame = page.frame_locator(
        'iframe[src*="assets.braintreegateway.com"][title*="Expiration"]'
    )
    expiry_frame.locator('[placeholder="MM/YY"]').fill("12/34")
    
    cvv_frame = page.frame_locator(
        'iframe[src*="assets.braintreegateway.com"][title*="CVV"]'
    )
    cvv_frame.locator('[placeholder="•••"]').fill("123")
    
    page.click("[data-testid='pay-button']")
    page.wait_for_url("**/confirmation**")
    assert page.locator(".order-number").is_visible()

Webhook Testing

PayPal IPN/webhooks require a publicly accessible URL. For local testing:

  1. Use ngrok: ngrok http 8000 — gives you https://abc123.ngrok.io
  2. Set this URL as your webhook endpoint in the PayPal developer portal
  3. Forward events to your local server

For Braintree webhooks:

def test_braintree_webhook_subscription_charged(gateway):
    # Braintree provides a notification testing endpoint
    sample_notification = gateway.webhook_testing.sample_notification(
        braintree.WebhookNotification.Kind.SubscriptionChargedSuccessfully,
        "subscription_id_123"
    )
    
    # Parse the notification (as your webhook handler would)
    notification = gateway.webhook_notification.parse(
        sample_notification.signature,
        sample_notification.payload
    )
    
    assert notification.kind == braintree.WebhookNotification.Kind.SubscriptionChargedSuccessfully
    assert notification.subscription.id == "subscription_id_123"

Common Gotchas

PayPal Sandbox accounts expire. Pre-created sandbox accounts can become stale. If tests suddenly fail with authentication errors, create fresh sandbox accounts in the developer portal.

Braintree sandbox transactions don't auto-settle. Unlike Stripe test mode, Braintree sandbox transactions require explicit settlement. Use gateway.testing.settle(transaction_id) to settle test transactions before testing refunds.

PayPal popup blockers interfere with browser tests. Playwright handles popups, but some CI environments block them. Configure your test runner to allow popups from your test domain.

Nonce expiry. Braintree nonces expire after a short period. Generate them close to use time in tests — don't create nonces in a setup fixture that runs before all tests.

Summary

PayPal and Braintree both provide complete sandbox environments. PayPal testing requires managing sandbox accounts and handling the OAuth flow. Braintree's nonce system makes server-side testing straightforward, but Drop-in UI automation requires careful iframe handling. Both platforms provide webhook simulation — use them to test your event handlers without waiting for real events.

Read more