Testing Stripe Checkout: Test Cards, Edge Cases, and End-to-End Flows
Testing Stripe checkout requires covering more than the happy path. 3D Secure authentication, declined cards, insufficient funds, SCA compliance, and idempotency all need explicit test coverage. This guide covers Stripe's test card catalog, Checkout Session API testing, and full E2E automation with Playwright.
Why Checkout Testing Is Harder Than It Looks
A payment form that "works" for you in development can fail for:
- Cards requiring 3D Secure authentication (common in Europe, UK)
- Cards that expire during the payment flow
- Network timeouts between your server and Stripe
- Idempotency key collisions on retry
- Webhook delivery failures after successful payment
If you haven't tested these scenarios, they will fail in production.
Stripe's Test Mode
All testing happens in test mode — a separate environment where no real money moves:
- Use test API keys (
sk_test_...,pk_test_...) - All test card numbers work only in test mode
- Webhooks can be tested locally with
stripe listen - Stripe's dashboard shows test transactions separately
# Switch environments in code
stripe = Stripe::Stripe::new(sk_test_key) <span class="hljs-comment"># Ruby
stripe = Stripe(api_key=sk_test_key) <span class="hljs-comment"># Python
const stripe = require(<span class="hljs-string">'stripe')(process.env.STRIPE_TEST_SECRET_KEY); <span class="hljs-comment"># NodeThe Test Card Catalog
Core Test Cards
| Scenario | Card Number | Exp | CVC |
|---|---|---|---|
| Success (Visa) | 4242 4242 4242 4242 | Any future | Any |
| Success (Mastercard) | 5555 5555 5555 4444 | Any future | Any |
| Success (Amex) | 3782 822463 10005 | Any future | Any 4-digit |
| Success (Debit) | 4000 0566 5566 5556 | Any future | Any |
| Declined — generic | 4000 0000 0000 0002 | Any future | Any |
| Declined — insufficient funds | 4000 0000 0000 9995 | Any future | Any |
| Declined — lost card | 4000 0000 0000 9987 | Any future | Any |
| Declined — stolen card | 4000 0000 0000 9979 | Any future | Any |
| Expired card | 4000 0000 0000 0069 | Any past | Any |
| Incorrect CVC | 4000 0000 0000 0101 | Any future | Any |
| Processing error | 4000 0000 0000 0119 | Any future | Any |
3D Secure Cards
| Scenario | Card Number |
|---|---|
| 3DS required | 4000 0027 6000 3184 |
| 3DS optional (supported) | 4000 0025 0000 3155 |
| 3DS required — fails | 4000 0084 0000 1629 |
| 3DS required — auth fails | 4000 0000 0000 3220 |
International and SCA Cards
| Scenario | Card Number |
|---|---|
| SCA authenticated (UK) | 4000 0082 6000 0000 |
| Indian cards (requires auth) | 4000 0035 6000 0008 |
| Brazilian cards | 4000 0007 6000 0002 |
Testing Payment Intents via API
Test payment scenarios programmatically:
import stripe
import pytest
stripe.api_key = "sk_test_your_key_here"
def create_test_payment_intent(amount: int, currency: str = "usd") -> stripe.PaymentIntent:
return stripe.PaymentIntent.create(
amount=amount,
currency=currency,
payment_method_types=["card"],
metadata={"test": "true"}
)
def test_payment_intent_created_with_correct_amount():
pi = create_test_payment_intent(2999) # $29.99
assert pi.status == "requires_payment_method"
assert pi.amount == 2999
assert pi.currency == "usd"
assert pi.id.startswith("pi_")
def test_confirm_with_success_card():
pi = create_test_payment_intent(5000)
# Attach a test payment method
pm = stripe.PaymentMethod.create(
type="card",
card={
"number": "4242424242424242",
"exp_month": 12,
"exp_year": 2028,
"cvc": "123"
}
)
# Confirm the payment intent
confirmed = stripe.PaymentIntent.confirm(
pi.id,
payment_method=pm.id
)
assert confirmed.status == "succeeded"
def test_declined_card_returns_card_error():
pi = create_test_payment_intent(5000)
pm = stripe.PaymentMethod.create(
type="card",
card={
"number": "4000000000000002", # decline card
"exp_month": 12,
"exp_year": 2028,
"cvc": "123"
}
)
with pytest.raises(stripe.error.CardError) as exc_info:
stripe.PaymentIntent.confirm(pi.id, payment_method=pm.id)
error = exc_info.value
assert error.code == "card_declined"
assert error.decline_code == "generic_decline"
def test_insufficient_funds_returns_correct_decline_code():
pi = create_test_payment_intent(5000)
pm = stripe.PaymentMethod.create(
type="card",
card={
"number": "4000000000009995", # insufficient funds
"exp_month": 12,
"exp_year": 2028,
"cvc": "123"
}
)
with pytest.raises(stripe.error.CardError) as exc_info:
stripe.PaymentIntent.confirm(pi.id, payment_method=pm.id)
assert exc_info.value.decline_code == "insufficient_funds"
def test_idempotency_prevents_duplicate_charges():
idempotency_key = "test-order-abc123-charge-v1"
pi1 = stripe.PaymentIntent.create(
amount=5000,
currency="usd",
payment_method_types=["card"],
idempotency_key=idempotency_key # same key = same result
)
pi2 = stripe.PaymentIntent.create(
amount=9999, # different amount — but same key
currency="usd",
payment_method_types=["card"],
idempotency_key=idempotency_key # returns first result, ignores this amount
)
# Same payment intent returned
assert pi1.id == pi2.id
assert pi1.amount == pi2.amount == 5000 # not 9999Testing Stripe Checkout Sessions
Stripe Checkout is the hosted payment page. Test that your session creation is correct:
def test_creates_checkout_session():
session = stripe.checkout.Session.create(
payment_method_types=["card"],
line_items=[{
"price_data": {
"currency": "usd",
"product_data": {"name": "Widget Pro"},
"unit_amount": 2999,
},
"quantity": 1,
}],
mode="payment",
success_url="https://example.com/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url="https://example.com/cancel",
)
assert session.status == "open"
assert session.payment_status == "unpaid"
assert session.url.startswith("https://checkout.stripe.com")
assert session.amount_total == 2999
def test_checkout_session_with_customer_email():
session = stripe.checkout.Session.create(
payment_method_types=["card"],
customer_email="alice@test.com",
line_items=[{
"price_data": {
"currency": "usd",
"product_data": {"name": "Widget"},
"unit_amount": 1000
},
"quantity": 1
}],
mode="payment",
success_url="https://example.com/success",
cancel_url="https://example.com/cancel"
)
assert session.customer_email == "alice@test.com"
def test_checkout_session_with_coupon():
# Create a test coupon
coupon = stripe.Coupon.create(
percent_off=20,
duration="once",
id="TEST20"
)
session = stripe.checkout.Session.create(
payment_method_types=["card"],
line_items=[{
"price_data": {
"currency": "usd",
"product_data": {"name": "Widget"},
"unit_amount": 5000
},
"quantity": 1
}],
discounts=[{"coupon": "TEST20"}],
mode="payment",
success_url="https://example.com/success",
cancel_url="https://example.com/cancel"
)
# 20% off $50 = $40
assert session.amount_total == 4000
# Cleanup
stripe.Coupon.delete("TEST20")E2E Checkout Flow with Playwright
Test the full checkout experience in a real browser:
# tests/e2e/test_stripe_checkout.py
import re
import pytest
from playwright.sync_api import Page, expect
STORE_URL = "https://your-store.test"
def fill_stripe_card(page: Page, card_number: str = "4242424242424242"):
"""Fill Stripe's card element (handles iframe)."""
# Stripe card element is in an iframe
card_frame = page.frame_locator('iframe[name*="__privateStripeFrame"]').first
card_frame.locator('[placeholder="Card number"]').fill(card_number)
card_frame.locator('[placeholder="MM / YY"]').fill("12 / 28")
card_frame.locator('[placeholder="CVC"]').fill("123")
card_frame.locator('[placeholder="ZIP"]').fill("97201")
def test_successful_checkout(page: Page):
page.goto(f"{STORE_URL}/products/widget-pro")
page.click("button:has-text('Buy Now')")
# Redirected to Stripe Checkout
expect(page).to_have_url(re.compile("checkout.stripe.com"))
# Fill email
page.fill('[placeholder="Email"]', "alice@test.com")
# Fill card details
fill_stripe_card(page, "4242424242424242")
# Submit
page.click('button:has-text("Pay")')
# Redirected back to success page
expect(page).to_have_url(re.compile("your-store.test/success"))
expect(page.locator("text=Thank you")).to_be_visible()
def test_declined_card_shows_error(page: Page):
page.goto(f"{STORE_URL}/products/widget-pro")
page.click("button:has-text('Buy Now')")
expect(page).to_have_url(re.compile("checkout.stripe.com"))
page.fill('[placeholder="Email"]', "alice@test.com")
# Declined card
fill_stripe_card(page, "4000000000000002")
page.click('button:has-text("Pay")')
# Error message should appear — NOT redirected
expect(page.locator("text=Your card has been declined")).to_be_visible()
expect(page).to_have_url(re.compile("checkout.stripe.com")) # stayed on checkout
def test_3ds_authentication_flow(page: Page):
page.goto(f"{STORE_URL}/products/widget-pro")
page.click("button:has-text('Buy Now')")
page.fill('[placeholder="Email"]', "alice@test.com")
fill_stripe_card(page, "4000002760003184") # 3DS required card
page.click('button:has-text("Pay")')
# 3DS modal appears
stripe_3ds_frame = page.frame_locator('[name="stripe-challenge-frame"]')
expect(stripe_3ds_frame.locator("text=Authenticate")).to_be_visible(timeout=10000)
# Complete 3DS (test mode has a button)
stripe_3ds_frame.locator("text=Complete authentication").click()
# Redirected to success
expect(page).to_have_url(re.compile("your-store.test/success"), timeout=15000)Testing Refunds
def test_full_refund():
# Create and confirm a payment
pm = stripe.PaymentMethod.create(
type="card",
card={"number": "4242424242424242", "exp_month": 12, "exp_year": 2028, "cvc": "123"}
)
pi = stripe.PaymentIntent.create(amount=5000, currency="usd", payment_method_types=["card"])
stripe.PaymentIntent.confirm(pi.id, payment_method=pm.id)
# Issue full refund
refund = stripe.Refund.create(payment_intent=pi.id)
assert refund.status == "succeeded"
assert refund.amount == 5000
def test_partial_refund():
pm = stripe.PaymentMethod.create(
type="card",
card={"number": "4242424242424242", "exp_month": 12, "exp_year": 2028, "cvc": "123"}
)
pi = stripe.PaymentIntent.create(amount=10000, currency="usd", payment_method_types=["card"])
stripe.PaymentIntent.confirm(pi.id, payment_method=pm.id)
# Partial refund ($30 of $100)
refund = stripe.Refund.create(payment_intent=pi.id, amount=3000)
assert refund.amount == 3000
# Verify PI still shows the remaining amount
pi_updated = stripe.PaymentIntent.retrieve(pi.id)
assert pi_updated.amount_received == 10000 # full amount receivedStripe Checkout Test Checklist
Before each release:
- Successful payment with Visa test card
- Successful payment with Mastercard test card
- Card declined — generic decline
- Card declined — insufficient funds
- Card declined — expired card
- 3D Secure flow — authenticate and complete
- 3D Secure flow — cancel authentication
- Coupon/discount applied correctly
- Refund — full refund
- Refund — partial refund
- Retry behavior on network error
- Webhook received after successful payment
- Order status updated after webhook
Next Steps
- See Stripe webhooks testing for webhook-specific test coverage
- Explore e-commerce regression testing for a complete release checklist
- Monitor with HelpMeTest — run Stripe checkout tests on a schedule and alert immediately if checkout breaks