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.

Every row is synthetic test data. No number here is issued by a bank, carries a balance, or will authorise anywhere. Never load this into a production database.

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Refresh expiry dates. A fixture pinned to 12/25 becomes 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.
  7. 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.

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.

Frequently Asked Questions

Ten thousand. The limit exists because everything runs in your browser tab — generation is chunked so it never freezes the page, but the whole set is held in memory and rendered into a single string on export. If you need more than that, generate several sets with different seeds and concatenate them, or move generation into your own build script with the npm or Composer package, which runs the same rules with no ceiling.
Yes, that is what the seed field is for. The same seed with the same settings produces byte-identical output, on any machine and any browser, because the generator uses a seeded pseudo-random function rather than the system random source. Leave the field blank and one is chosen for you and displayed, so you can reproduce a set after the fact.
Yes. Set a percentage of rows to carry a deliberately wrong check digit, and every record is tagged with a luhn_valid flag so your fixtures can be split into positive and negative cases without re-deriving anything. A validation test suite that only contains valid input is only testing half of the behaviour.
JSON if your tests read fixtures directly, because it parses into objects with no work. JSONL if the set is large and you want to stream it. CSV or TSV for spreadsheets and data-loading tools. SQL when you are seeding a database directly. The format only affects packaging — the records themselves are identical.
Within a single set, yes. Duplicates are detected and regenerated as the set is built. Across two separate runs with different seeds, collisions are possible but very unlikely at these volumes. Note that the last four digits are not unique and cannot be — with a thousand possible combinations, any set over a few dozen rows will repeat them, which is a useful property to test against.
No. Beyond the obvious — none of it will authorise — synthetic records in a production system are actively harmful, because someone will eventually find them and be unable to tell whether they represent real customers. Keep test data in test environments, and tag it at creation so it is identifiable if it ever escapes.