A credit card validator test checks one narrow thing: does your payment form accept the numbers it should accept and reject the ones it should not? You run it in a sandbox with published test card numbers, never with real cardholder data. A solid test set covers the Luhn checksum, the length and prefix rules for each brand, the expiry date logic, and the CVV field format. If all four hold up, your validator is doing its job.

The four checks behind every validator

  • Luhn checksum. A single check digit catches typos and transposed digits.
  • Length and prefix. Visa starts with 4 and runs 13 to 19 digits. Mastercard uses 51 through 55 and 2221 through 2720. Amex is 15 digits starting 34 or 37.
  • Expiry. A card expiring this month is still live. One expiring last month is not.
  • CVV format. Three digits for most brands, four for Amex. Format only. You cannot verify the value without an authorization request.

Luhn, in plain terms

Start from the rightmost digit and move left. Double every second digit. If a doubled digit lands above 9, subtract 9. Add everything together. A valid number gives a total divisible by 10. That is the whole algorithm, and it takes about ten lines of code in any language.

Worth knowing: a random string of 16 digits passes Luhn roughly one time in ten. So a Luhn pass means the number is well formed, not that it exists or belongs to anyone. Treat it as a typo filter, not a security control.

Where to get test numbers

Use the test cards your processor publishes. Stripe, PayPal, Adyen, and Authorize.net each maintain a set, and those numbers are wired to specific sandbox responses: approved, declined, insufficient funds, expired card. That lets you test both the happy path and the failure path without touching a live account.

Do not make up a number and expect it to pass. If you need an arbitrary valid test string, generate one and compute the check digit yourself, then confirm it against your processor's test set before you trust it.

Test cases worth writing down

  1. A valid number for each brand you support.
  2. A number one digit off, which must fail Luhn.
  3. A transposition of two adjacent digits, also failing Luhn.
  4. A correct-checksum number that is too short and too long for its brand.
  5. A number with spaces and dashes, which should be stripped before validation.
  6. An expiry of last month, this month, and next year.
  7. A three-digit CVV on an Amex number, which should fail.
  8. Empty fields, whitespace only, and letters mixed into digits.

Client side is convenience, server side is the rule

Browser validation exists to give fast feedback. Anyone can bypass it with a form post or a curl call, so every check needs to run again on your server before you hand the number to the processor. I look for one shared validation module used in both places, because two implementations drift apart within a month.

Mistakes I see most

  • Trusting Luhn as fraud detection. It is a checksum, nothing more.
  • Hardcoding a card length of 16 for every brand.
  • Rejecting numbers with spaces or dashes, which real users type constantly.
  • Logging the full number during debugging. Now it lives in your log aggregator forever.
  • Testing only with one brand and calling it done.

One compliance note

PCI DSS forbids storing the CVV, and the full card number, once an authorization is complete. Keep test data in the sandbox, keep real numbers out of your logs, and store only a token plus the last four digits. That habit costs nothing to build in early and is painful to retrofit later.