Comparison

Why not just write it yourself?

Nearly everyone does, at first — an array of first names, an array of streets, Math.random(), and you have fixtures by lunchtime. It works, right up until the moment your data has to be valid. This is an honest account of what comes after that moment, including when writing it yourself is still the better answer.

Side by side

Generating people, addresses and national identifiers for a test suite — the job a home-grown helper is usually written for.

Writing your ownReal Fake Data
Getting startedAn afternoon — a name array, a random index, doneOne HTTP call, or one line with an addon
Valid checksumsYou implement each algorithm yourself, per countryImplemented and tested per country already
Real addressesYou source, clean and ship a register — or invent street namesBuilt from the national registers themselves
Adding a second countryThe work starts over: new algorithm, new register, new edge casesChange one segment of the path
Keeping it correctRegisters move and formats change; it becomes your bug when they doMaintained upstream, with a changelog
Deliberately invalid recordsA second code path you also have to write and keep honestA flag — the checksum breaks, everything else stays intact
Works with no networkRuns in-process; depends on nobodyAn HTTP call — the service has to be reachable

The afternoon is real. So is what follows it.

The first version genuinely does take an afternoon, and for a while it is entirely sufficient. Random digits in the right shape are fine as long as nothing checks them.

Then something checks them. A registration form gains validation, or an integration test starts calling the same validator production uses, and every fixture in the suite fails at once. The fix is not a tweak — the number has to be *derived* rather than *drawn*, which is a different program.

The version after that is the one that costs: the same work again for the next country, because none of it transfers. A PESEL and a Danish CPR share no arithmetic.

the afternoon version, and the real one
// Monday. Ships, works, everyone is happy.
const pesel = () =>
  String(Math.floor(Math.random() * 1e11)).padStart(11, '0');

// Eleven digits. Looks exactly like a PESEL.
// Fails every validator that actually checks one.

// What it has to be instead:
//   digits 1-6   a real date, century encoded in the month
//   digits 7-10  serial, last of them encodes sex
//   digit 11     derived from the ten before it

Each checksum is small. Twenty-seven of them are not.

None of these algorithms is hard in isolation. A PESEL applies the weights 1 3 7 9 1 3 7 9 1 3 and takes the result modulo 10 — an hour, including the tests. The trouble is that the hour does not amortise.

A Danish CPR uses different weights and a modulo 11 — and then a rule you would have no reason to go looking for. The seventh digit, read together with the two-digit year, is what disambiguates the birth century: 5–8 means the 1800s for years 58–99 but the 2000s for years 00–57. And the mod-11 check itself stopped being guaranteed in 2007, so a generator that is actually correct has to be able to produce numbers that are valid Danish CPRs *and* fail the checksum.

An Italian codice fiscale is barely arithmetic at all. Most of its length is an encoding of the name — consonants first, then vowels, with a different rule when the given name has four or more consonants — plus a birth month written as one of twelve non-sequential letters. Only the final character is computed, from two separate conversion tables depending on whether a position is odd or even. An IBAN is a third shape again: move the first four characters to the end, substitute each letter for a two-digit number, then take the whole thing modulo 97.

Six countries in, this is no longer a helper file. It is a subsystem with its own tests, its own edge cases, and its own bugs — and it is nobody at your company’s actual job.

four countries, four unrelated algorithms
PL  PESEL            weights 1 3 7 9 1 3 7 9 1 3, mod 10
DK  CPR              different weights, mod 11
                     + a century lookup on the 7th digit
                     + the check is not guaranteed post-2007
IT  codice fiscale   name -> consonants then vowels,
                     month -> one of A B C D E H L M P R S T,
                     check char from two conversion tables
--  IBAN             rotate 4, letters -> two digits, mod 97

None of them shares a line of code with any other.

Seeding is not a feature you can add later

Sooner or later you want a failing test to replay with exactly the data that broke it. That means a seed — and JavaScript does not have one. `Math.random()` cannot be seeded; the language offers no equivalent of `srand`.

So you bring your own PRNG, and then you discover the harder half: the seed has to reach every function that draws a value. One un-threaded `Math.random()` anywhere in the tree — in the street picker, in the gender coin-flip, in the fallback branch nobody looks at — and reproducibility is silently gone. You will not notice, because the output still looks fine.

This is why the generators here are pure functions that receive their randomness through a context rather than reaching for it. It is not sophistication; it is the only arrangement that makes the guarantee hold, and it is much easier to adopt at the start than to retrofit.

the seed has to reach everywhere
Math.random()   // cannot be seeded. JavaScript has
                // no srand, and never has.

// So: bring a PRNG, and thread it through every
// single function that draws a value.

person(rng)
  -> address(rng)      // threaded
       -> street(rng)  // threaded
       -> postcode()   // <- forgot this one

// Output still looks perfect. Reproducibility is
// gone, and nothing will tell you.

When writing your own is the right answer

If you need one country and one format, write it yourself. A single national identifier is an afternoon that stays an afternoon, and a dependency you do not have is a dependency that cannot break. The argument on this page is about the second country, not the first.

If your tests must run with no network — an air-gapped build, a hermetic CI sandbox — an HTTP call is the wrong shape regardless of how good the data is. Generate a fixture file once and commit it; seeded output makes that a stable artefact rather than a snapshot that drifts.

And for data with no rules to satisfy — filler text, random integers, arbitrary timestamps — a faker library remains the right tool. Correct-by-construction matters where something downstream does the checking. Where nothing does, it buys you nothing.

Compare it against the one you already wrote

Take a national identifier your own helper produces, and one from here, and run both through the validator your application uses. That comparison takes a minute and settles the question.