Most test data is lying to you.
123 Test Street, Testville, 12345 passes every form you build, so the form looks fine. Then a real customer from Berlin types Hauptstraße 12, someone in Toronto enters M5V 3L9, and a user in Dubai leaves the postcode empty because their country doesn't have one. That's when the bug reports come in.
I spent the last few months building a set of random data generators, and the most useful part turned out not to be the "random" bit. It was making the data realistic enough to break things the way real data does. Here's what I learned, with code you can reuse.
1. Use a fair random source (and avoid the modulo trap)
Math.random() is fine for a dice game, but it isn't cryptographically secure, and the common pattern below is slightly biased:
// Biased: some values come up more often when 2^32 isn't divisible by the range
const n = crypto.getRandomValues(new Uint32Array(1))[0] % 6;
Rejection sampling removes the bias. Throw away the values from the "uneven tail" and draw again:
function randomInt(min, max) {
const range = max - min + 1;
const limit = Math.floor(0x100000000 / range) * range; // largest multiple of range below 2^32
const buf = new Uint32Array(1);
let x;
do {
crypto.getRandomValues(buf);
x = buf[0];
} while (x >= limit);
return min + (x % range);
}
For a six-sided die the redraw almost never happens, but for passwords, PINs and tokens it's worth getting right. It's three extra lines.
2. Addresses are not "street, city, zip" everywhere
These are the format differences that broke the most forms I tested:
-
Germany, Netherlands, Switzerland: the house number comes after the street:
Hauptstraße 12,Prinsengracht 263. -
Japan: addresses identify blocks rather than streets:
1-4-30 Higashi, written smallest unit first in the Western order, plus a 7-digit postal code like272-1249. -
United Kingdom: postcodes have an outward and an inward part with a space in between (
SW1A 2AA), and letters and digits are mixed. -
Canada: letter-digit-letter, space, digit-letter-digit (
K1A 0B1). A^\d{5}$regex rejects every Canadian. -
Ireland: Eircodes (since 2015) look like
D02 X285. -
Brazil: an 8-digit CEP written
01310-100. - United Arab Emirates: there is generally no postcode. If your postcode field is required, you've just blocked a whole country.
A trick that made the generated data much more convincing: tie the postal code to the city. Pick a real city first, then generate a code from that city's own pattern. German postcodes for Munich start with 80/81, UK postcodes for Leeds start with LS, and Canadian codes in Toronto start with M. The data then passes "does this postcode belong to this region?" checks too, and looks right to someone who lives there.
I store each city as a small row and fill the patterns with tokens:
// [city, region, regionCode, postalPattern, areaCode]
const cities = [
['Munich', 'Bavaria', 'BY', '80###', '89'],
['Hamburg', 'Hamburg', 'HH', '2####', '40'],
];
// '#' = any digit, '%' = 1-9, '@' = uppercase letter
function fill(pattern) {
return pattern.replace(/[#%@]/g, t =>
t === '#' ? String(randomInt(0, 9)) :
t === '%' ? String(randomInt(1, 9)) :
String.fromCharCode(65 + randomInt(0, 25)));
}
3. Phone numbers: the trunk prefix trap
Most countries put a "trunk prefix" (usually 0) in front of domestic numbers and drop it internationally:
| Country | Domestic | International |
|---|---|---|
| UK | 020 7946 0958 |
+44 20 7946 0958 |
| Germany | 030 1234567 |
+49 30 1234567 |
| Italy | 06 1234 5678 |
+39 06 1234 5678 ← keeps the 0! |
If your phone normaliser always strips a leading 0 after the country code, every Italian landline breaks. If it never strips it, every UK number breaks. Generate numbers in national, international and E.164 (+442079460958) formats and run all of them through your parser.
Tip: if you show phone numbers in demos or screenshots, use ranges reserved for fiction. In the US that's 555-0100 to 555-0199; in the UK Ofcom reserves ranges such as 07700 900000–07700 900999 for drama. For emails, use the reserved domains example.com / example.org (RFC 2606) so a test never emails a real person.
4. Numbers with checksums
Lots of identifiers include a check digit, and validators reject random digits instantly. Two worth knowing:
Luhn (payment card test numbers):
// Returns the digit that makes `payload + digit` Luhn-valid
function luhnCheckDigit(payload) {
let sum = 0;
let double = true; // the rightmost payload digit gets doubled
for (let i = payload.length - 1; i >= 0; i--) {
let d = Number(payload[i]);
if (double) { d *= 2; if (d > 9) d -= 9; }
sum += d;
double = !double;
}
return (10 - (sum % 10)) % 10;
}
const payload = '4' + Array.from({ length: 14 }, () => randomInt(0, 9)).join('');
const card = payload + luhnCheckDigit(payload); // 16-digit, Luhn-valid Visa-format number
These are format-valid test numbers only: they aren't linked to any account and won't authorise a real payment. For actual payment flows, use your provider's official test cards.
IBAN (mod 97):
function ibanCheckDigits(country, bban) {
// Move "CC00" to the end, turn letters into numbers (A=10 … Z=35), then mod 97
const digits = (bban + country + '00').toUpperCase()
.replace(/[A-Z]/g, c => String(c.charCodeAt(0) - 55));
let r = 0;
for (const ch of digits) r = (r * 10 + Number(ch)) % 97; // piecewise: no BigInt needed
return String(98 - r).padStart(2, '0');
}
const bban = '37040044' + '0532013000'; // bank code + account number (German layout)
const iban = 'DE' + ibanCheckDigits('DE', bban) + bban;
The same idea applies to ISBN-13, EAN barcodes (alternating weights of 1 and 3), US routing numbers (weights 3-7-1) and VINs (position 9 is a check digit).
5. A quick checklist for your next form
- Does the postcode field accept letters, spaces and dashes?
- Is "State/Region" optional for countries that don't use one?
- Does a 60-character street name survive storage, PDFs and email templates?
- Do accents (
São Paulo,Düsseldorf,Kraków) survive a round trip through your API and database? - Does phone parsing handle Italian landlines and E.164?
- Are names allowed to have one part, or the family name first (common in China, Japan and Korea)?
Running five or six countries through this list caught more bugs for me than a week of testing with US-only data.
The tool I ended up building
All of this became RandNow, a free collection of random generators. The address, name and phone tools cover 40 countries using the rules above, and there are generators for Luhn-valid test cards, IBANs, UUIDs/ULIDs, VINs, ISBNs and JSON fixtures. Everything runs client-side with crypto.getRandomValues(), so nothing is sent to a server, and you can export CSV or JSON for your test suite.
If you live in one of those 40 countries and spot a format that looks wrong, I'd genuinely like to hear about it in the comments — that's the kind of detail only locals notice.
Please use generated data for testing, demos and privacy only. Never use it to impersonate someone or commit fraud.
Top comments (0)