What a Stripe test card actually is
A Stripe test card is a fake card number that only resolves in test mode. It runs the same gauntlet a real card would: Luhn check, expiry, CVC format, billing ZIP, and the network rules tied to that BIN range. What it never does is move money. No bank gets contacted, no authorization is requested, and nothing shows up on a statement.
The part people trip over is that the number does not decide the environment. Your API key does. Send 4242 4242 4242 4242 with a live secret key and Stripe rejects it as an invalid card. Send a real Visa number with a test key and it fails too. Test mode and live mode are separate worlds, and the keys are the door.
Getting into test mode
- Flip the Test mode toggle in the Dashboard.
- Copy the test publishable key and the test secret key.
- Swap both into your environment config. Do not edit them into source.
If you use the CLI, stripe listen forwards test webhooks to your local machine so you can watch payment_intent.succeeded land in real time instead of guessing.
The numbers you will reach for most
- 4242 4242 4242 4242: Visa, always approves.
- 4000 0566 5566 5556: Visa debit.
- 5200 8282 8282 8210: Mastercard debit.
- 5555 5555 5555 4444: Mastercard credit.
- 3782 822463 10005: American Express, 15 digits.
- 6011 1111 1111 1117: Discover.
For all of these, any future expiry date works. Three digits for the CVC, four for Amex. Any well-formed billing ZIP. The success path is the easy part, which is exactly why testing only that path is a mistake.
Forcing declines on purpose
Declines come from dedicated numbers, not from mistyping a good one. 4000 0000 0000 0002 returns a generic card_declined. 4000 0000 0000 9995 returns insufficient_funds. 4000 0000 0000 0069 is an expired card, 4000 0000 0000 0127 is an incorrect CVC, and 4000 0000 0000 0119 is a processing error. If your checkout branches on the decline_code field, each of those gives you a different branch to exercise.
I keep a small fixture file with one number per code and loop through it in CI. It catches the sad path where your UI shows a blank error because you only handled the generic case.
Authentication and 3D Secure
3DS is where integrations quietly break, because a passing unit test never touches the challenge iframe. Stripe ships test numbers that force the authentication step. 4000 0025 0000 3155 requires 3DS and succeeds once you complete it. 4000 0084 0000 1629 requires it and fails. 4000 0000 0000 3220 forces the challenge to complete and then gets declined anyway, which is the scenario that exposes bad error handling.
Run these in an actual browser against a real test key. A mocked SDK response will not tell you what happens when the customer closes the modal.
Wallets and other methods
Card numbers cover cards. Apple Pay and Google Pay have their own test setups and generally need a real device or a supported browser profile, not just a number in a form field. For bank debits and other local methods, the test credentials live in the same testing docs, organized by payment method rather than by BIN.
Mistakes that show up in review
- Hardcoding 4242 in a shared fixture and never testing a decline.
- Assuming Radar rules and test clocks behave the same way in test mode. They do not mirror live traffic.
- Letting a test key reach a production build. This is the one that costs you sleep.
- Storing test card data in a place that suggests it is sensitive. It is not cardholder data, and treating it like it is wastes effort.
One last note on scope: test cards prove your code handles Stripe's responses. They cannot tell you how a real issuer will behave, what your fraud rules should be, or whether a specific BIN will approve. Those are production questions, and the only honest way to answer them is with real traffic and real monitoring.