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 own | Real Fake Data | |
|---|---|---|
| Getting started | An afternoon — a name array, a random index, done | One HTTP call, or one line with an addon |
| Valid checksums | You implement each algorithm yourself, per country | Implemented and tested per country already |
| Real addresses | You source, clean and ship a register — or invent street names | Built from the national registers themselves |
| Adding a second country | The work starts over: new algorithm, new register, new edge cases | Change one segment of the path |
| Keeping it correct | Registers move and formats change; it becomes your bug when they do | Maintained upstream, with a changelog |
| Deliberately invalid records | A second code path you also have to write and keep honest | A flag — the checksum breaks, everything else stays intact |
| Works with no network | Runs in-process; depends on nobody | An 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.
// 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 itEach 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.
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.
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.