Jest Test Data: Generated JSON Fixtures for Jest and Vitest
Jest test data is usually whatever was quickest to type. A test needs
a user, so it gets an object literal; the next test file needs one
too, so the literal gets copied and one field changes. Or the suite
pulls in @faker-js/faker, calls it without a seed, and every run
sees different values. Vitest suites inherit the same habits, since
the testing API is nearly identical. This post builds a third option:
a JSON fixture file generated once from a seeded template, committed
to the repo, and loaded by a small helper that every test file shares.
Valid cases and deliberately broken ones sit in one test.each
table, each row carries its expected outcome, snapshots stay stable
because the data under them never changes, and any run can be rebuilt
from its seed.
JsonFabrica's role here is small. It's a JSON-over-HTTP API: a short
Node script sends it a template and a seed and writes the generated
JSON to test/fixtures/. There is no npm package for tests and no
Jest or Vitest plugin, and your test runs never call the API. It's the
JavaScript counterpart of
generated pytest fixtures; if your backend
is in Python, the same template and fixture file can serve both
suites.
Where Jest test data usually comes from
Two patterns show up in almost every JavaScript codebase.
Inline object literals. A test declares
const room = { id: "r1", city: "Lisbon", nightlyRate: 120 }, and
the next file copies it. Literals are readable and stable, but they
stay small and they drift. Nobody types forty records, so tests only
ever see two. When the Room type gains a field, half the copies get
updated and the other half quietly test an object shape the app no
longer produces.
Unseeded faker calls. A beforeEach calls
faker.location.city() and faker.number.int(), so there's as much
realistic data as you like. It brings its own problems:
- Failures don't reproduce. Without
faker.seed(), every run gets new values. A test that fails on CI because one generated value hit an edge case passes on your machine, and nothing in the log tells you which value it was. - Related records don't line up. Each call is independent. A
booking's
roomIdpoints at a real room only if the test wires it up, and a "closed room" test works only if something happens to create one. - Nobody can review the data. The values exist only while the tests run. You can't open a file and see which cases the suite covers.
Both are fine in small doses. The trouble starts when several test files share the same records, when records reference each other, or when a failure on one machine has to be replayed on another.
Generate once, commit the JSON, read it in tests
The alternative splits the work into two steps that never run at the same time:
- Generate offline. A regen script sends a template and a fixed
seed to the JsonFabrica API and writes the response to
test/fixtures/bookings.json. You run it by hand when the template changes, review the diff, and commit the file. - Load in tests. A helper module reads the committed file. CI needs no API key, makes no network calls, and tests exactly the bytes you reviewed.
The project layout for the rest of this post:
package.json
vitest.config.js # only if you use Vitest
src/
bookings.js # code under test
scripts/
regen-fixtures.mjs # calls the API; run by hand, not in CI
test/
fixtures/
bookings.tmpl # the template (committed)
bookings.json # the generated data (committed)
fixtures.js # shared helper every test file imports
createBooking.test.js # the test.each case table
fixtureData.test.js # checks the file's own invariants
roomLabel.test.js # snapshot test
The code under test is a small booking function with four rules: the room must exist, the room must be open, a booking is for 1 to 8 guests, and it lasts 1 to 30 nights. There's also a label formatter, which the snapshot test uses later.
// src/bookings.js
export class BookingError extends Error {
constructor(code) {
super(code);
this.name = "BookingError";
this.code = code;
}
}
export function createBooking(rooms, { roomId, guests, nights }) {
const room = rooms.find((r) => r.id === roomId);
if (!room) throw new BookingError("room_not_found");
if (room.status !== "open") throw new BookingError("room_closed");
if (!(guests >= 1 && guests <= 8)) throw new BookingError("invalid_guests");
if (!(nights >= 1 && nights <= 30)) throw new BookingError("invalid_nights");
return { roomId, guests, nights, total: room.nightlyRate * nights };
}
export function formatRoomLabel(room) {
const closed = room.status === "open" ? "" : " (closed)";
const place = `${room.city}, ${room.address}`;
return `${room.id} · ${place} · ${room.nightlyRate}/night${closed}`;
}
Every example in this post is an ES module ("type": "module" in
package.json) and was run under both Jest 30.5 and Vitest 5.0 on
Node 24. Vitest runs ES modules as-is. For Jest, the examples were run
with Jest's native ESM support
(NODE_OPTIONS=--experimental-vm-modules npx jest), which Jest's
documentation still describes as experimental. If your Jest setup compiles tests to CommonJS
with babel-jest or ts-jest instead, the tests work the same; only the
fixture path in the helper changes, as noted below.
A template for Jest and Vitest test data
One template produces both halves of the fixture file: a list of rooms, and a table of booking cases that reference those rooms. Keeping them in one template is what keeps them consistent. A case can only point at a room ID that the same template just wrote.
<setVar('rooms', getParam('rooms', 6))>{
"rooms": [
<for(i, 1, getVar('rooms'))><if(getVar('i') > 1)>,<endIf>
{
"id": "rm_<appendBefore('0', getVar('i'), 3)>",
"city": "<getRandomCity()>",
"address": "<getRandomStreet()>",
"nightlyRate": <getRandomNumber(60, 240)>,
"status": <if(getVar('i') == 1)>"closed"<else>"open"<endIf>
}<end_for>
],
"bookingCases": [
<for(i, 1, getParam('cases', 12))><if(getVar('i') > 1)>,<endIf>
{
"id": "case-<appendBefore('0', getVar('i'), 2)>",
"case": <if(getVar('i') == 1)>"unknown-room"
<elseIf(getVar('i') == 2)>"zero-nights"
<elseIf(getVar('i') == 3)>"too-many-guests"
<elseIf(getVar('i') == 4)>"closed-room"
<else>"valid"<endIf>,
"roomId": <if(getVar('i') == 1)>"rm_999"
<elseIf(getVar('i') == 4)>"rm_001"
<else>"rm_<appendBefore('0', getRandomNumber(2, getVar('rooms')), 3)>"
<endIf>,
"guestEmail": "<getRandomEmail('example.com')>",
"guests": <if(getVar('i') == 3)>9<else><getRandomNumber(1, 8)><endIf>,
"nights": <if(getVar('i') == 2)>0<else><getRandomNumber(1, 14)><endIf>,
"expectedError": <if(getVar('i') == 1)>"room_not_found"
<elseIf(getVar('i') == 2)>"invalid_nights"
<elseIf(getVar('i') == 3)>"invalid_guests"
<elseIf(getVar('i') == 4)>"room_closed"
<else>null<endIf>
}<end_for>
]
}
What each part does:
roomsis the only variable, because it's read twice.getParamreads the room count from the request, defaulting to 6. The room loop uses it as its end bound, and the valid cases use it as the upper limit for the room they book. Everything else is written inline in its own field, including the case count, which is read once as the second loop's bound.- IDs come from the loop index, not from randomness.
appendBeforepads the index, so the rooms are alwaysrm_001torm_006and the casescase-01tocase-12. Stable IDs make stable test names. - Valid cases can only book real, open rooms.
getRandomNumberpicks a room number from 2 up to the room count, so a valid row never points at a room that doesn't exist, and never atrm_001. Keeproomsat 2 or more. - The planted cases occupy fixed rows. Rows 1 to 4 each break
exactly one rule: an unknown room, zero nights, nine guests, and a
booking for
rm_001, the one room the first loop always marks as closed. That last case only works because the rooms and the cases come from the same template. One defect per row means a failure can only have one cause. - Every row carries its expected outcome.
expectedErroris the error code the row should produce, ornullfor a valid booking.
Template expressions support comparisons (==, !=, <, <=, >,
>=) and &&/||, but no arithmetic, so "exactly four invalid rows"
is written as fixed index checks rather than a counter. Loop bounds
are inclusive, the loop variable is read with getVar('i'), and the
comma guard <if(getVar('i') > 1)>,<endIf> puts a comma before every
array item except the first. The control flow docs
cover for and if/elseIf/else. Putting the planted rows first
means case-04 is always the closed-room case, however many valid
rows you ask for.
Rendered with seed 20261009 and the default parameters, the file
the regen script writes starts like this (trimmed to two rooms and
five cases):
{
"seed": 20261009,
"rooms": [
{
"id": "rm_001",
"city": "Medan",
"address": "Franklin Gardens 9",
"nightlyRate": 105,
"status": "closed"
},
{
"id": "rm_002",
"city": "Bahia Blanca",
"address": "Madison Court 158",
"nightlyRate": 113,
"status": "open"
}
],
"bookingCases": [
{
"id": "case-01",
"case": "unknown-room",
"roomId": "rm_999",
"guestEmail": "[email protected]",
"guests": 4,
"nights": 11,
"expectedError": "room_not_found"
},
{
"id": "case-02",
"case": "zero-nights",
"roomId": "rm_006",
"guestEmail": "[email protected]",
"guests": 2,
"nights": 0,
"expectedError": "invalid_nights"
},
{
"id": "case-03",
"case": "too-many-guests",
"roomId": "rm_005",
"guestEmail": "[email protected]",
"guests": 9,
"nights": 10,
"expectedError": "invalid_guests"
},
{
"id": "case-04",
"case": "closed-room",
"roomId": "rm_001",
"guestEmail": "[email protected]",
"guests": 5,
"nights": 9,
"expectedError": "room_closed"
},
{
"id": "case-05",
"case": "valid",
"roomId": "rm_004",
"guestEmail": "[email protected]",
"guests": 6,
"nights": 4,
"expectedError": null
}
]
}
The planted rows are otherwise ordinary. case-02 books an open room
for two guests, so the only thing wrong with it is the zero nights.
case-04 has sensible guests and nights, so the only thing wrong with
it is the room's status.
The regen script: one fetch call, one committed file
The regen script is the only code that talks to JsonFabrica. It's
plain Node (20 or later, for the built-in fetch and parseArgs),
sends the raw template to
POST /v1/templates/generate, and
writes the result. That endpoint renders a template without saving
it, so the template stays a file in your repo, next to the data it
produces.
#!/usr/bin/env node
// scripts/regen-fixtures.mjs
// Regenerate test/fixtures/bookings.json from bookings.tmpl.
// Run by hand when the template changes, then review and commit the
// result. CI never runs this script; it only reads the committed file.
import { readFile, writeFile } from "node:fs/promises";
import { parseArgs } from "node:util";
const API_URL = "https://api.jsonfabrica.com/v1/templates/generate";
const DIR = new URL("../test/fixtures/", import.meta.url);
const DEFAULT_SEED = 20261009; // change only to replace the data on purpose
const { values: args } = parseArgs({
options: {
seed: { type: "string", default: String(DEFAULT_SEED) },
"random-seed": { type: "boolean", default: false },
cases: { type: "string", default: "12" },
},
});
const request = {
body: await readFile(new URL("bookings.tmpl", DIR), "utf8"),
params: { rooms: 6, cases: Number(args.cases) },
};
if (!args["random-seed"]) request.seed = Number(args.seed);
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.JSONFABRICA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (!response.ok) {
// The body is {"error": {"code", "message", "details"}}, e.g.
// INVALID_TEMPLATE, UNKNOWN_FUNCTION, or GENERATION_FAILED.
console.error(response.status, await response.text());
process.exit(1);
}
const { data, meta } = await response.json();
if (request.seed !== undefined && meta.seed !== request.seed) {
console.error(`API used seed ${meta.seed}, expected ${request.seed}`);
process.exit(1);
}
const out = new URL("bookings.json", DIR);
const json = JSON.stringify({ seed: meta.seed, ...data }, null, 2);
await writeFile(out, json + "\n");
const n = data.bookingCases.length;
console.log(`wrote ${out.pathname} (seed ${meta.seed}, ${n} cases)`);
Add it to the scripts in package.json so nobody has to remember
the path:
"fixtures:regen": "node scripts/regen-fixtures.mjs"
Pass flags after --, as in npm run fixtures:regen -- --cases 20.
The details that matter:
- The response is
{ data, meta }.datais the generated document, already parsed.meta.seedechoes the seed the API used: the one you sent or, if you sent none, one it picked. - The seed goes into the file. Writing
meta.seedas a top-levelseedkey means the fixture records how to rebuild itself.meta.generatedAtis left out on purpose: it changes on every call and would make every regeneration show a diff. - The seed check catches a seed that didn't arrive. A mistyped
--seed abcbecomesNaN, whichJSON.stringifysends asnull. Either the API rejects the request, or it picks its own seed andmeta.seeddoesn't match theNaNthat was asked for. Both stop the script before it writes a file nobody can regenerate. - Same template, seed and params give the same file. Running the
script twice produces no diff. Raising
--casesappends rows and leaves the existing ones as they were, because the case loop runs after the rooms and every earlier row consumes the same random draws. Changing the room count or editing the template shifts the draws, so commit the regenerated file in the same pull request as the template change.
Loading JSON fixtures for JavaScript unit tests
There are two ways to get the file into a test: import it as a module, or read and parse it yourself. Both work in Jest and Vitest.
Importing it. The standard syntax is an import attribute:
import data from "./fixtures/bookings.json" with { type: "json" };
In the versions tested here, that line worked in Jest 30 (native ESM), in Vitest 5, and in plain Node 24. A few variations behave differently:
assert { type: "json" }is the older, deprecated form of the same thing. Node 24 rejects it with a syntax error, and so does the Babel parser Jest 30 uses. Vitest 5 still accepted it, but there's no reason to write it in new code.- A bare
import data from "./bookings.json"worked in both test runners, because they resolve JSON files themselves. Plain Node rejects it withERR_IMPORT_ATTRIBUTE_MISSING, so a module that is also used outside the test runner needs the attribute. - Named imports such as
import { seed } from "./bookings.json"worked in Vitest, which exposes top-level JSON keys as named exports, and failed in Jest with "does not provide an export named 'seed'". Use the default import if a file has to run under both. - In CommonJS tests,
require("./fixtures/bookings.json")works in Jest. TypeScript projects also needresolveJsonModuleintsconfig.jsonto type-check JSON imports.
Reading it in a helper. An imported JSON module is one shared
object, and every test in the file gets the same instance. If one
test sets rooms[1].status = "closed", every later test in that file
sees a closed room, and results start depending on test order. A
small helper that reads the file and hands out copies avoids that, and
gives every test file one place to get its data from:
// test/fixtures.js: the one place that knows where the fixture file is.
import { readFileSync } from "node:fs";
const file = new URL("./fixtures/bookings.json", import.meta.url);
const data = JSON.parse(readFileSync(file, "utf8"));
export const fixtureSeed = data.seed;
// Frozen, because test.each reads it while the file is being collected
// and every test in the file shares the same rows.
export const bookingCases = Object.freeze(
data.bookingCases.map((c) => Object.freeze(c)),
);
// A fresh deep copy per call, so no test can change another's rooms.
export function loadRooms() {
return structuredClone(data.rooms);
}
How the pieces fit:
- The path is built from the module's own URL.
new URL(..., import.meta.url)resolves relative tofixtures.js, so the helper works whether the runner is started from the repo root, a package directory in a monorepo, or an IDE. In a Jest setup that compiles to CommonJS,import.metaisn't available; usepath.join(__dirname, "fixtures", "bookings.json")instead. - Rooms are copied, cases are frozen. Rooms are records tests may
legitimately modify, so
loadRooms()returns a new deep copy every time. The case table is read-only input, so it's frozen instead; test files are ES modules and run in strict mode, so a stray assignment such asbookingCases[0].guests = 3throws aTypeErrorrather than quietly changing a row other tests use. - The seed is exported. The case table uses it in its test names, which pays off when a run needs to be reproduced.
Jest provides describe, test, expect and the hooks as globals.
Vitest doesn't by default: either import them from "vitest" at the
top of each test file, or turn globals on once in the config, which
is what the examples below assume:
// vitest.config.js
import { defineConfig } from "vitest/config";
export default defineConfig({
test: { globals: true },
});
A test.each test data table with expected outcomes
The booking cases become a test.each table. Every row is one test,
and because each row says what should happen, one test body covers
the valid bookings and the planted invalid ones. it.each is the same
function under the it alias, in both runners.
// test/createBooking.test.js
import { createBooking } from "../src/bookings.js";
import { bookingCases, fixtureSeed, loadRooms } from "./fixtures.js";
describe(`createBooking (fixture seed ${fixtureSeed})`, () => {
let rooms;
beforeEach(() => {
rooms = loadRooms();
});
test.each(bookingCases)("$id $case", (c) => {
const book = () => createBooking(rooms, c);
if (c.expectedError === null) {
const room = rooms.find((r) => r.id === c.roomId);
expect(book()).toEqual({
roomId: c.roomId,
guests: c.guests,
nights: c.nights,
total: room.nightlyRate * c.nights,
});
} else {
expect(book).toThrow(
expect.objectContaining({ code: c.expectedError }),
);
}
});
});
A few things are worth spelling out:
$id $casenames each test from its row. When the table rows are objects, both runners replace$propertyin the title with that property's value. Vitest's verbose reporter prints (file prefix and timings trimmed):✓ createBooking (fixture seed 20261009) > case-01 unknown-room ✓ createBooking (fixture seed 20261009) > case-02 zero-nights ✓ createBooking (fixture seed 20261009) > case-03 too-many-guests ✓ createBooking (fixture seed 20261009) > case-04 closed-room ✓ createBooking (fixture seed 20261009) > case-05 validBecause the IDs come from the template's loop index, they're stable, so
npx vitest run -t "case-04"always runs the same row. Jest needs native ESM switched on for theseimport-based files, so run it asNODE_OPTIONS=--experimental-vm-modules npx jest -t "case-04".Invalid rows must fail for the right reason.
toThrow(expect.objectContaining({ code }))checks the error'scode, not just that something was thrown. Suppose someone deletes theroom_closedcheck and the guest-count check has a bug that rejectscase-04's five guests. The closed room now falls through to that check and throwsinvalid_guests. A test that only checked "it throws" would still pass; checking the code fails it.Valid rows check the computed total. The template can't do arithmetic, so the expected total isn't in the file. The test looks up the row's room and computes it, which also proves the case points at a room that exists.
The same idea, a table where every row carries the result it expects, also drives API runs outside a unit test framework; the Postman test data post applies it to a Newman data file with expected status codes.
Why the table can't come from beforeAll
beforeEach above gives every test a fresh copy of the rooms, which
is the right place for anything a test might modify. beforeAll is
the place for read-only setup that is worth doing once per file. A
second test file checks the fixture's own invariants, and builds a
lookup map once:
// test/fixtureData.test.js: checks the fixture file's own invariants.
import { bookingCases, loadRooms } from "./fixtures.js";
const valid = bookingCases.filter((c) => c.expectedError === null);
let roomsById;
beforeAll(() => {
roomsById = new Map(loadRooms().map((r) => [r.id, r]));
});
test.each(valid)("$id books an existing, open room", (c) => {
expect(roomsById.get(c.roomId)?.status).toBe("open");
});
The test body uses roomsById from beforeAll, but the table itself,
valid, is built at module level, and that's not a style choice. Both
runners evaluate the argument to test.each while they collect the
file, before any hook has run. Assign the table inside beforeAll and
it's still undefined at collection time: Jest 30 reports
".each must be called with an Array or Tagged Template Literal",
and Vitest 5 fails the file with a TypeError. Tables come from
module scope, which is one more reason to keep them in a helper
module.
This invariant test is cheap insurance. If someone edits the template
and a valid case starts pointing at the closed room, this file fails
with a clear name, instead of createBooking failing with a confusing
room_closed.
Snapshot tests that stay stable
Snapshot tests are where unseeded data hurts most. A snapshot of
anything built from faker.location.city() without a seed changes on
every run, so the test either fails constantly or gets updated until
nobody reads the diff. With a committed fixture, the input never
changes, so the snapshot only changes when the code does:
// test/roomLabel.test.js
import { formatRoomLabel } from "../src/bookings.js";
import { loadRooms } from "./fixtures.js";
test("room labels", () => {
expect(loadRooms().map(formatRoomLabel)).toMatchSnapshot();
});
Both runners write the snapshot to
test/__snapshots__/roomLabel.test.js.snap. With the fixture rendered
from seed 20261009, the stored value is:
exports[`room labels 1`] = `
[
"rm_001 · Medan, Franklin Gardens 9 · 105/night (closed)",
"rm_002 · Bahia Blanca, Madison Court 158 · 113/night",
"rm_003 · Ibadan, Willow Street 88 · 132/night",
"rm_004 · Kumamoto, Smith Boulevard 35 · 235/night",
"rm_005 · Dhanbad, Davis Drive 128 · 185/night",
"rm_006 · Cologne, Chestnut Way 72 · 185/night",
]
`;
The data is varied enough to be worth snapshotting: one closed room,
six cities, a spread of prices. It's also fixed, so the snapshot only
changes when formatRoomLabel does. Some practical rules:
- Commit the
.snapfile with the fixture. In CI, both runners refuse to write a snapshot that doesn't exist yet: Jest with--ci(or when it detects a CI environment), Vitest when theCIenvironment variable is set. A missing snapshot fails the run instead of passing silently. - A regenerated fixture means a reviewed snapshot update. If a
template change alters the rooms, run the tests with
-u(both runners accept it), and review the fixture diff and the snapshot diff in the same pull request. - Exploratory runs on fresh data should skip snapshots. A snapshot pins exact output for exact input, so it will always fail on data generated from a different seed. That's covered next.
The same committed-fixture approach keeps visual snapshots stable for components, too; the Storybook mock data post builds fixtures for each component state, and a component test can read the same JSON file the stories use.
Reproduce a failing run from its seed
With a committed file, most reproduction is trivial. CI used the same
bookings.json you have locally, and the failing test's name tells
you which row broke. Introduce a bug that rejects bookings for more
than six guests, and Jest reports:
● createBooking (fixture seed 20261009) › case-07 valid
Vitest names the same test, with > between the parts. case-07
books seven guests, which is valid, so -t "case-07" reruns exactly
that row while you fix it.
The seed earns its keep when you go looking for data you didn't plan for. A nightly exploratory job can regenerate the fixture with a seed the API picks, run the case tables against it, and throw the file away afterwards. It needs the API and an API key, so keep it separate from the CI runs that gate merges, and leave snapshot tests out of it:
node scripts/regen-fixtures.mjs --random-seed --cases 40
npx vitest run test/createBooking.test.js test/fixtureData.test.js
The regen script prints the seed it received, and the describe
title puts it into every test name:
wrote /home/ci/app/test/fixtures/bookings.json (seed 601148902, 40 cases)
When the nightly job fails, take the seed from its log and regenerate locally with the same parameters, from the same commit so the template matches:
node scripts/regen-fixtures.mjs --seed 601148902 --cases 40
npx vitest run -t "case-17"
Same template, same seed, same parameters: the file is byte-for-byte the one the nightly job tested, and the failure reproduces. The seed and case ID in that example are placeholders; use the ones from your own log. The deterministic test data post covers capturing and replaying seeds in more depth.
Once you've found the bug, decide what to keep. Usually the failing
row points at a case the planted rows didn't cover. Add it as a new
planted row in the template, regenerate with the default seed, and
commit the template, the data and any updated snapshot together. If
you're not changing the template, git restore test/fixtures/bookings.json puts the committed file back.
When seeded faker or fishery is the simpler choice
Generate-once-and-commit is extra machinery: a template, a script, an API key for whoever regenerates, and a JSON file to review. For a lot of tests, that's more than the job needs.
Use seeded faker for small, local variations. A signup test that
needs a plausible email, a form test that fills fields nobody asserts
on, a test file that wants twenty slightly different names: calling
@faker-js/faker inline is simpler than any file. Seed it so a
failure can be replayed:
import { faker } from "@faker-js/faker";
beforeEach(() => {
faker.seed(123);
faker.setDefaultRefDate("2026-01-01T00:00:00.000Z");
});
Seeding in beforeEach matters: faker's output depends on the seed
and on how many values have been drawn since it was set, so a seed set
once per file makes each test's values depend on the tests before it.
setDefaultRefDate matters too. Methods such as faker.date.past()
are relative to the current time: in a check with faker 10.6, two
calls after the same seed returned past() dates one millisecond
apart, because the clock had moved. Faker's documentation also notes that a new faker version can
produce different values for the same seed, so pin the version if an
assertion depends on exact output. And faker.seed() with no argument
picks a random seed and returns it, which you can log for the same
replay trick as above.
Use a factory library such as fishery for object variations inside
one test file. A factory defines a default room once, and each test
calls something like roomFactory.build({ status: "closed" }) to get
the record it needs with only the relevant field overridden. When each
test builds exactly the objects it needs, a factory reads better than
a lookup into a shared file.
Generate and commit when the data is shared or has to be replayed. The file-based approach wins when:
- Several test files, or several projects, share one data set. The same JSON can feed a Python suite, Storybook stories or a Postman collection, which a fishery factory can't.
- Runs must reproduce across machines and CI. The committed bytes are the data. There's no dependency on a faker version, the clock, test order, or how many values another test drew first.
- Records must stay consistent across the set. A closed room that exactly one case targets, valid cases that only book open rooms: these are properties of the whole data set, set in one template, not of one object built in isolation.
- Edge cases are planted and reviewed. The invalid rows sit in a file a reviewer reads in the pull request, each with its expected error, instead of being spread across factory overrides and test bodies.
The approaches mix well. A suite can load its shared rooms and case table from the committed file, and still call a seeded faker inside a single test for a throwaway value nobody asserts on.
FAQ
How do I import a JSON file in Jest?
In a Jest test that runs as an ES module,
import data from "./fixtures/data.json" with { type: "json" } works,
and in CommonJS tests require("./fixtures/data.json") works. Use the
default export: Jest does not expose a JSON file's top-level keys as
named imports. The older assert { type: "json" } syntax is
deprecated, and Jest 30 and Node 24 both reject it. Reading the file
with fs.readFileSync and JSON.parse in a small helper module also
works, and behaves the same in Jest, Vitest and plain Node.
Can test.each use data loaded in beforeAll?
No. Jest and Vitest evaluate the table passed to test.each while
they collect the test file, before any beforeAll or beforeEach
hook has run, so a variable assigned in beforeAll is still
undefined at that point. Load the table at module level instead,
for example from a fixtures helper module. The test body can still use
anything a hook set up.
How do I make faker return the same data every time in Jest or Vitest?
Call faker.seed() with a fixed number, ideally in beforeEach, so
every test starts from the same sequence regardless of test order.
Also call faker.setDefaultRefDate() with a fixed date, because
methods such as faker.date.past() are relative to the current time
and change even with a seed. Faker's documentation notes that output
for a seed can change when you upgrade Faker, so pin the version if
assertions depend on exact values.
How do I share test data between test files in Jest?
Put the data in a JSON file and give it one small helper module that reads it and exports what tests need, such as the case table and a function that returns a fresh copy of mutable records. Every test file imports from that helper instead of declaring its own object literals. Returning copies keeps one test from changing data another test relies on.
Does JsonFabrica have a Jest or Vitest plugin?
No. JsonFabrica is an HTTP API that returns generated JSON, and there
is no npm package for tests. You call the API from a short script with
fetch or curl, commit the JSON file it writes, and load that file in
your tests. Jest and Vitest never call the API, so CI needs no API key
or network access.
A seeded template, a short regen script and one committed file
give a Jest or Vitest suite shared fixtures, a test.each table with
planted invalid cases, snapshots that only change when code does, and
runs you can replay from a seed. Seeded, parameterized generation is
part of the JsonFabrica API, and the
templates API reference documents the
generate request and response.
Generate realistic test data with JsonFabrica
Describe the shape of your data once, then generate as many fresh, realistic JSON documents as you need via a simple API call.