Anatomy of Stripe test data
Stripe provides test card numbers that simulate real behaviors without touching money. '4242424242424242' is the success wildcard: always works, useful for smoke tests. '4000000000009995' simulates generic decline. The difference is in the check digit: Stripe validates it with Luhn algorithm, so random numbers fail.
Tokens like 'tok_visa' are shortcuts for quick testing in Stripe dashboard, but in real code you should use Stripe.js to tokenize. Payment Methods ('pm_card_visa') are the modern API; tokens are legacy but still valid for backward compatibility.
Resource IDs (cus_, ch_, pi_) follow patterns: prefix + underscore + alphanumeric string. In test mode, any string after the prefix is valid ('cus_123', 'cus_fake') as long as the prefix matches the resource type. In production, Stripe generates real 24-28 character IDs.
Testing strategy: beyond the happy path
The most common error: only testing '4242424242424242' and assuming everything works. Your code must handle at least 5 scenarios: success, generic decline, requires_action (3DS), processing error, insufficient funds. Each returns different response structures.
For 3D Secure (SCA), use '4000002760003184'. This card number triggers the complete flow: your frontend must show the auth modal, user 'approves', and only then the payment succeeds. If your test passes without modal, you didn't implement SCA correctly and will fail in European production.
Webhooks are pain point #1. In local dev, use Stripe CLI ('stripe listen --forward-to localhost:3000/webhook') to receive real events. Test idempotency: Stripe can send the same webhook multiple times; your endpoint must be idempotent (check 'event.id' before processing).
Edge cases that break integrations
Decimal amounts: Stripe uses integers, not floats. $10.00 = 1000 (in USD). Yen has no decimals, ¥1000 = 1000. If you pass '10.00' as a number, Stripe rejects it. Correct conversion: Math.round(amount * 100) for currencies with 2 decimals.
Metadata limits: you can pass up to 50 metadata keys, each value max 500 characters. Useful for internal tracking, but if you try to pass a giant serialized JSON, it silently fails. Test with excessive metadata to catch this edge case.
Out-of-order webhooks: you can receive 'payment_intent.succeeded' BEFORE 'payment_intent.created'. Stripe doesn't guarantee order. Your logic must be stateless or check current resource state with a GET before processing. Classic bug: assuming 'created' always arrives first and setting flags that don't update later.
Robust test environment setup
Key separation: use different environment variables for test/prod. Common pattern: STRIPE_SECRET_KEY in prod, STRIPE_TEST_SECRET_KEY in dev. Never commit keys to git. Use local .env and secrets management in production (AWS Secrets Manager, Doppler, etc).
For CI/CD, create a separate Stripe test account (don't use your personal account). This allows multiple devs and pipelines to run tests without collisions. Each PR can create ephemeral resources (customers, subscriptions) that clean up automatically after.
Test data factories: instead of hardcoding '4242424242424242' in each test, create helpers: createSuccessfulCard(), createDeclinedCard(), create3DSCard(). This centralizes test data and makes updating easier if Stripe changes numbers. Example: when 3DS2 replaced 3DS1, you only had to update one function instead of 50 tests.