Random test data is useful.
Until a test fails.
Then randomness can become your enemy.
Imagine a CI job fails with a generated dataset containing a specific combination of:
- name
- country
- date
- order value
- status
- identifier
You run the test again.
It passes.
Run it a third time.
It passes again.
The failure is gone because the generator created different data.
This is exactly where seeded synthetic data becomes useful.
A seed lets you keep the benefits of generated test data while making the output reproducible.
What does a seed actually do?
A deterministic generator starts with an initial value:
seed = 42
The generator uses that value as the starting state for producing pseudo-random values.
If the algorithm and configuration remain the same:
seed = 42
should produce the same sequence again.
Conceptually:
seed 42
↓
Anna
Hamburg
20095
€184.22
active
Run it again with the same seed:
seed 42
↓
Anna
Hamburg
20095
€184.22
active
Change the seed:
seed 43
and you get a different dataset.
That makes the seed a compact way to describe an entire generated fixture.
Why this matters in CI
Consider a test like this:
test("checkout accepts generated customer data", async () => {
const customer = generateCustomer();
const result = await checkout(customer);
expect(result.success).toBe(true);
});
If generateCustomer() returns different values every time, a failure can be difficult to reproduce.
Instead, imagine:
const customer = generateCustomer({
seed: "checkout-1842"
});
Now your CI output can report:
Test failed.
Seed: checkout-1842
A developer can run the test locally using exactly the same seed.
That turns:
"It failed once in CI."
into:
"Run this exact dataset."
That is a huge debugging improvement.
Randomness is not the same as unpredictability
Developers sometimes assume that test data should always be completely unpredictable.
Usually it should not.
Production systems may need cryptographically secure randomness.
Test fixtures usually need something else:
controlled variation.
You want datasets that differ enough to expose assumptions while still being reproducible when something breaks.
For testing, deterministic pseudo-randomness is often more useful than true randomness.
A simple seeded generator
Here is a deliberately small example:
function createRng(seed) {
let state = seed >>> 0;
return function () {
state = (state * 1664525 + 1013904223) >>> 0;
return state / 4294967296;
};
}
Now:
const random = createRng(12345);
console.log(random());
console.log(random());
console.log(random());
Calling the same function again with:
createRng(12345)
produces the same sequence.
For real applications, you would normally use a mature deterministic PRNG library rather than inventing one.
The important idea is the deterministic relationship:
same seed + same algorithm = same sequence
Build fixtures from the seeded source
Once you have a seeded random source, other values should derive from it.
For example:
function pick(random, values) {
const index = Math.floor(random() * values.length);
return values[index];
}
Then:
const random = createRng(42);
const country = pick(random, ["DE", "FR", "NL"]);
const status = pick(random, ["active", "pending", "blocked"]);
If you recreate the generator with seed 42, you should receive the same choices.
That is the foundation of reproducible synthetic data.
The whole pipeline must be deterministic
A common mistake is seeding only one part of the generator.
For example:
const random = seededRandom(seed);
const name = generatedFromSeed(random);
const email = generatedFromSeed(random);
const createdAt = new Date();
The name and email are reproducible.
The timestamp is not.
If reproducibility matters, avoid hidden dynamic inputs such as:
current time
machine hostname
process ID
random UUID
timezone
environment-specific ordering
Instead, make them explicit inputs.
For example:
generateFixture({
seed: 42,
now: "2026-10-05T12:00:00Z"
});
Now both randomness and time are controlled.
Version your fixture generators
There is another subtle problem.
Imagine this configuration:
seed = 42
generates:
{
"name": "Anna Schneider",
"country": "DE"
}
Then six months later you improve your generator.
You add more names and change the country selection algorithm.
Now seed 42 might produce:
{
"name": "Jonas Weber",
"country": "NL"
}
Technically, the seed stayed the same.
But the generator changed.
If you rely heavily on reproducibility, save the generator version too:
generatorVersion = 3
seed = 42
A failure report can then contain:
Fixture generator: v3
Seed: 42
That is much more reliable than storing only the seed.
Seeds should describe scenarios
You do not have to use numbers.
Readable seeds can make test output easier to understand.
For example:
checkout-basic
checkout-eu-user
checkout-large-order
import-invalid-address
profile-long-name
Internally, the string can be hashed into a numeric seed.
This gives your tests meaningful names while preserving deterministic generation.
Instead of:
generateUser(91281);
you can write:
generateUser("signup-german-user");
The second version communicates intent.
Don't make every test depend on huge generated profiles
Synthetic-data systems can generate enormous records.
That does not mean every test should use them.
Suppose you are testing:
email normalization
You probably need:
{
"email": "Test.User@Example.com"
}
You probably do not need:
{
"name": "...",
"address": "...",
"phone": "...",
"company": "...",
"job": "...",
"iban": "...",
"device": "...",
"orderHistory": []
}
Large fixtures increase cognitive load.
Generate only what the test actually needs.
A useful principle is:
Small deterministic fixtures for unit tests, richer synthetic datasets for integration and system tests.
Separate normal fixtures from edge cases
Pure randomness is surprisingly bad at finding many important bugs.
A generator may happily produce normal data thousands of times without ever creating:
an empty string
a 255-character name
February 29
a negative value
a duplicate identifier
a zero-value order
a Unicode-heavy address
So combine generated fixtures with deliberate edge cases.
For example:
const cases = [
generateCustomer("normal-customer"),
generateCustomer("another-customer"),
{
...generateCustomer("long-name"),
name: "A".repeat(255)
}
];
Randomness should complement scenario design, not replace it.
Keep relationships between fields deterministic
Realistic fixtures often contain dependent fields.
For example:
country = DE
city = Hamburg
postalCode = 20095
phonePrefix = +49
A poor generator might independently produce:
country = DE
city = Toronto
postalCode = SW1A 1AA
phonePrefix = +81
That may be useful for some validation tests.
But it is not a realistic German profile.
If your application depends on relationships between fields, derive those values from the same deterministic context.
Conceptually:
const country = generateCountry(random);
const address = generateAddress({
random,
country
});
const phone = generatePhone({
random,
country
});
Now the fixture stays coherent while remaining reproducible.
Save the seed when reporting errors
If generated data is used in automated tests, always include the seed in failure output.
For example:
try {
await runCheckoutTest(fixture);
} catch (error) {
console.error(`Fixture seed: ${seed}`);
throw error;
}
Your CI logs might then show:
FAIL checkout.integration.test.js
Expected: 200
Received: 500
Fixture seed: checkout-regression-9381
That one line may save an hour of debugging.
Seeds are also useful outside automated tests
Reproducible datasets help with more than CI.
Bug reports
Instead of attaching a huge JSON file:
Use seed: bug-2841
UI development
Designers and frontend developers can repeatedly work with the same profile dataset.
Database demos
Seeded data keeps screenshots and demonstrations consistent.
Performance testing
Specific seeds can represent different dataset shapes.
For example:
small-shop
enterprise-customer
high-order-volume
QA
A tester can send a developer:
Generator: profile
Seed: qa-login-114
Both sides can reproduce the same data.
But don't mistake seeds for security
A deterministic seed is not a secret.
If someone knows:
seed = customer-demo
and knows the generator algorithm, they may be able to reproduce the same data.
That is completely fine for synthetic test fixtures.
It is not fine for generating:
- passwords
- encryption keys
- API tokens
- password-reset tokens
- session secrets
Security-sensitive values require cryptographically secure generation.
Seeded fixture generation and secure random generation solve different problems.
A practical test-data strategy
A good setup might contain three layers.
Layer 1: Explicit fixtures
For exact business rules:
{
"price": 100,
"discount": 10
}
Layer 2: Seeded synthetic fixtures
For realistic variation:
seed = checkout-customer-42
Layer 3: Randomized exploratory tests
For discovering unexpected combinations:
random seed generated per run
The trick is to log the random seed.
If an exploratory run fails:
Random seed: 9182811
you can convert an unpredictable failure into a reproducible one.
Generate random seeds — but record them
This gives you a useful hybrid approach.
At test startup:
const seed =
process.env.TEST_SEED ??
Math.floor(Math.random() * 1_000_000);
Then print it:
console.log(`TEST_SEED=${seed}`);
Your CI run might use:
TEST_SEED=718421
If the test fails, reproduce it locally:
TEST_SEED=718421 npm test
Now each CI execution explores new data while every failure remains reproducible.
That is often the best of both worlds.
Using Genory for reproducible datasets
You can experiment with synthetic test data using the Genory Test Data tools.
For dataset-style workflows, Genory can help generate structured test records while keeping development data separate from real customer information.
For programmatic workflows and supported endpoints, the current API documentation is available at:
Whether you use Genory, a local fixture library, or your own generator, the underlying principle remains the same:
configuration
+
seed
+
generator version
=
reproducible fixture
A useful failure report
Instead of logging an entire fixture, you may be able to log:
Generator: customer-profile
Version: 3
Seed: checkout-regression-9381
Locale: de-DE
Now: 2026-10-05T12:00:00Z
That is enough information to reconstruct the dataset.
It is compact, searchable and easy to paste into a bug report.
Final thought
Generated test data should not make bugs harder to reproduce.
It should make bugs easier to discover.
The key is controlling randomness instead of eliminating it.
Use synthetic data to introduce variation.
Use seeds to make that variation repeatable.
And whenever a randomized test fails, save the seed.
Because:
A random failure you cannot reproduce is frustrating.
A random failure with a seed is a test case.
Top comments (0)