What a card validation test actually checks

A card validation test verifies the shape of a card number, not the account behind it. You check the length, the issuer identification number, the Luhn check digit, the expiry format, and the length of the security code. Then you send the number to a sandbox and assert on the response. All of it runs on test PANs from your processor. None of it needs a real card.

That distinction matters. Format validation is a client and server side hygiene step. Authorization is a conversation between your acquirer and the issuer. Mixing the two is where test suites rot.

Start with the Luhn checksum

The Luhn algorithm walks the digits right to left, doubles every second digit, subtracts 9 from anything above 9, and sums the lot. A total divisible by 10 passes. I treat it as a cheap typo catcher at the form level. It catches single-digit slips and most swapped neighbors, which is exactly what you want before you burn an authorization attempt.

It tells you nothing else. A number can pass Luhn and still belong to a closed account, a blocked BIN range, or nobody at all. Only the issuer, answering an authorization request that your own processor sends, can say whether the payment will clear. Keep the two stages separate in your code and your tests.

A test matrix worth running

  • Valid checksum, supported BIN, future expiry: expect a successful tokenization or approval in sandbox.
  • One digit changed so the checksum fails: expect a validation error before any network call.
  • Too few or too many digits: expect a length rejection, not a gateway timeout.
  • Expired date, and a date far in the future: both should be caught at the edge.
  • Security code that is too short, too long, or non-numeric.
  • Unsupported BIN or card brand you do not accept.
  • Processor decline scenarios: insufficient funds, do not honor, expired card. Assert on the specific decline code, not just the word failure.
  • Three-domain Secure challenge path, including the customer abandoning it.

Run the same matrix at the API layer, not only in the browser. A form that blocks a bad checksum means nothing if your endpoint accepts it anyway.

Test numbers come from your processor

Every major gateway publishes sandbox PANs for each brand, plus a set of numbers that trigger particular decline codes. Use those. They work only in test mode, which is the whole point. If a test number returns an approval on a live endpoint, something is badly misconfigured, and that is a finding worth a ticket.

Do not invent numbers and do not borrow one from a colleague's wallet, even with permission. Test data should be synthetic from end to end so your logs, your fixtures, and your screenshots stay clean.

Mistakes I see often

  • Logging the full PAN in debug output. That drags your log stack into PCI scope.
  • Storing the security code anywhere after authorization. It should be dropped the moment the request is sent.
  • Testing only the happy path and calling the integration done.
  • Asserting on a generic error string instead of the processor's response code.
  • Skipping the retry and idempotency cases, which is where duplicate charges come from.

Compliance guardrails

PCI DSS is blunt about sensitive authentication data: the CVV is used to authorize a payment and must not be retained afterward, in any form. Tokenization helps here. Swap the PAN for a processor token as early as you can and let the token travel through your systems instead of the number itself. When you write validation tests, include a check that the security code never appears in your database, your queue payloads, or your logs. That test is boring and it has saved teams from real incidents.