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
- Go to developer.paypal.com
- Log in with your PayPal account
- Navigate to Sandbox > Accounts
- You'll see pre-created sandbox business and personal accounts
- 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 & Credentials → Sandbox)
- 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 braintreeConfigure 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:
- Use ngrok:
ngrok http 8000— gives youhttps://abc123.ngrok.io - Set this URL as your webhook endpoint in the PayPal developer portal
- 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.