Bulk synthetic payment test data for QA teams and automation engineers: up to ten thousand records with reproducible seeds, a configurable share of deliberately broken rows, and export to the format your suite actually reads. Everything is generated in your browser and downloaded directly — no data is uploaded, and nothing is stored.
Test data only
Bulk Test Card Generator
Up to 10,000 synthetic records with reproducible seeds and negative cases. Generated in your browser and downloaded directly — nothing is uploaded.
Generating…
Output formats
Every format carries the same records; only the packaging differs. These samples are real
output from the tool above, generated with the seed docs-sample.
CSV
card_number,network,exp_month,exp_year,cvv,cardholder_name,luhn_valid,expired
5231698649355089,mastercard,09,2030,435,Quinn Placeholder,true,false
4391134838583520,visa,02,2030,005,Taylor Sandbox,true,false
4251988068019342,visa,08,2028,128,Quinn Placeholder,true,false
4374741431393462,visa,10,2027,995,Riley Dummy,false,false
Look at the CVV on the second row: 005. Open this file in a spreadsheet application and
that becomes 5, while the card numbers become floating-point values in scientific notation.
Both are silent, and both produce test data that no longer tests anything. If a CSV has to go
through a spreadsheet, import the columns as text rather than opening the file directly — or
use JSON, where the values are quoted and the problem does not exist.
JSON
[
{
"cardNumber": "5231698649355089",
"network": "mastercard",
"expMonth": "09",
"expYear": "2030",
"cvv": "435",
"cardholderName": "Quinn Placeholder",
"luhnValid": true,
"expired": false
}
]
JSONL — one record per line, for streaming a large set without parsing it all at once:
{"cardNumber":"5231698649355089","network":"mastercard","expMonth":"09","expYear":"2030","cvv":"435","cardholderName":"Quinn Placeholder","luhnValid":true,"expired":false}
{"cardNumber":"4391134838583520","network":"visa","expMonth":"02","expYear":"2030","cvv":"005","cardholderName":"Taylor Sandbox","luhnValid":true,"expired":false}
SQL
INSERT INTO test_payment_methods
(card_number, network, exp_month, exp_year, cvv, luhn_valid, expired)
VALUES
('5231698649355089', 'mastercard', '09', '2030', '435', TRUE, FALSE),
('4391134838583520', 'visa', '02', '2030', '005', TRUE, FALSE);
Note that every value is quoted as text. A card number in a numeric column loses leading zeros and precision, and the same applies to security codes — these are digit strings, not integers.
TSV is the same as CSV with tab separators, which is the safer choice when a cardholder name might contain a comma.
Using bulk test data in your test suite
Playwright
import cards from './fixtures/test-cards.json';
test.describe('checkout accepts all supported networks', () => {
for (const card of cards.filter(c => c.luhnValid)) {
test(`accepts ${card.network} ${card.cardNumber.slice(0, 4)}...`, async ({ page }) => {
await page.goto('/checkout');
await page.fill('[name=cardNumber]', card.cardNumber);
await page.fill('[name=cvv]', card.cvv);
await expect(page.locator('[data-brand]')).toHaveText(card.network);
});
}
});
pytest, with the negative half of the suite that most examples leave out:
import json
import pytest
with open('fixtures/test_cards.json') as f:
CARDS = json.load(f)
@pytest.mark.parametrize('card', [c for c in CARDS if c['luhnValid']])
def test_card_is_accepted(client, card):
resp = client.post('/validate-card', json={'number': card['cardNumber']})
assert resp.status_code == 200
assert resp.json()['network'] == card['network']
@pytest.mark.parametrize('card', [c for c in CARDS if not c['luhnValid']])
def test_invalid_card_is_rejected(client, card):
resp = client.post('/validate-card', json={'number': card['cardNumber']})
assert resp.status_code == 400
The luhn_valid flag is what makes both halves come from one file. Without it you would be
maintaining two fixtures and hoping they stay in step.
Test data management principles
The tool is the easy part. These seven habits are what separate a fixture set that helps from one that quietly rots:
- Never use production card data in a test environment. PCI DSS requires it: version 4 states in Requirement 6.5.5 that live PANs are not used in pre-production environments, tightening the wording of what was Requirement 6.4.3 under version 3.2.1. Synthetic data is a compliance obligation, not a convenience.
- Version your fixtures. Commit the generated file rather than generating at test time. A suite that builds its own inputs on every run is a suite whose failures you cannot reproduce.
- Seed deterministically. Same seed, same data, same result. If you must generate at runtime, pin the seed in the repository and print it on failure.
- Include negative cases. Broken check digits, wrong lengths, expired dates, a three-digit code on an Amex number. Validation you have never seen fail is validation you have not tested.
- Cover every network you accept. At minimum one card per brand you take, and specifically American Express at fifteen digits and a 2-series Mastercard — the two cases hard-coded rules break on.
- Refresh expiry dates. A fixture pinned to
12/25becomes an expired-card fixture on a date nobody chose, and the resulting failure looks like a code regression. Generate expiry relative to today, or regenerate the file on a schedule. - Mask in logs, even for test data. The habit is what transfers. A logger that prints a full number in staging will print one in production the first time someone reuses the helper.
What the seed actually buys you
Reproducibility is worth more than it first appears, and it is the feature most bulk generators skip.
The obvious win is debugging. When a parameterised suite fails on row 4,127 of a generated set, a seed turns “it failed once in CI” into a set you can regenerate on your laptop and step through. Without one, you are reading a stack trace about data that no longer exists.
The less obvious win is review. A seeded fixture file produces a clean diff — regenerate with the same seed after changing a setting and the only rows that move are the ones the setting affected, so a reviewer can see what changed rather than a ten-thousand-line replacement. Put the seed in a comment at the top of the fixture, or in the filename, and the file documents how to rebuild itself.
The trap to avoid is treating a seed as a guarantee across versions. It fixes the sequence of random numbers, not the code that consumes them: change the network mix, the invalid share, or the generator itself, and the same seed yields a different set. That is correct behaviour, not a bug — but it means a seed identifies a set only alongside the settings that produced it. Record both.
Realistic distribution
Test data should look like your traffic. If seventy per cent of your real payments are Visa and half your fixtures are American Express, your suite spends its time on a path your users rarely take while under-covering the one they do — and the bugs it finds are weighted the same way.
The realistic mix option approximates a typical Western e-commerce split at sixty per cent Visa, thirty per cent Mastercard and ten per cent American Express. Treat that as a starting point, not a fact about your business: pull the actual distribution from your processor’s reporting and match it. If a tenth of your volume is a domestic scheme, a tenth of your fixtures should be too.
Limits and performance
Ten thousand records is the ceiling, and the reason is the browser rather than the arithmetic. Generation is chunked in batches of five hundred with the main thread released between each one, so the page stays responsive and the progress bar is honest — but the full set lives in memory and export serialises all of it into a single string. Past ten thousand, that serialisation is what starts to hurt, not the generating.
If you need more, two options. Generate several sets with different seeds and concatenate them, which also gives you a natural way to shard fixtures across test suites. Or lift the generation function out of this page — view source, take the seeded random function and the check-digit calculation, and run it in your own build script where there is no tab to freeze. The BIN generator does the same job for a single prefix, and the validator will confirm any row you are suspicious of.
For processor behaviour rather than format coverage, generated data is the wrong input entirely — the test card numbers reference lists the sandbox cards that produce real approvals, declines and 3-D Secure challenges.
Related tools and guides
Generate single records with the all-network generator, or build the matching billing addresses, postcodes and contact details with the test identity generator — the two together give you a complete payment-form fixture rather than a column of numbers. Our test data management guide goes further into fixture strategy. The tool directory lists everything else, and the FAQ covers what a Luhn-valid number does and does not prove.