Reusable Test Data Templates: One Template, Every Scenario
A test suite that covers "valid input," "invalid input," "free-tier
user," "paid-tier user," and "empty account" tends to grow one template
file per case: account-valid.json, account-invalid.json,
account-free.json, account-paid.json, account-empty.json. Every one
of those files is identical except for two or three fields, and every
time the account shape gains a new field, all five files need the same
edit. Reusable test data templates fix this the other way around: write
the shared shape once, and read the handful of values that actually
change with getParam, so a single template produces every scenario
depending on what you pass it at generation time.
The problem: N near-duplicate templates for N scenarios
Here's what that duplication usually looks like in practice — three
templates for the same account entity, differing only in tier and
itemCount:
// account-free.json
{
"id": "<createSeq('acctId', 'uuid')><getSeq('acctId')>",
"tier": "free",
"itemCount": 0,
"email": "<getRandomEmail()>"
}
// account-paid.json
{
"id": "<createSeq('acctId', 'uuid')><getSeq('acctId')>",
"tier": "paid",
"itemCount": 5,
"email": "<getRandomEmail()>"
}
// account-paid-empty.json
{
"id": "<createSeq('acctId', 'uuid')><getSeq('acctId')>",
"tier": "paid",
"itemCount": 0,
"email": "<getRandomEmail()>"
}
Three files, three near-identical bodies, and a fourth scenario — say, "free tier with a populated cart" — means a fourth file. Every field the account shape shares gets copy-pasted three or four times, and every shared-field edit has to land in all of them or the templates quietly drift out of sync with each other.
Reusable test data templates: read the difference with getParam
getParam(name, default?) reads a value from the params object sent in
the generation request, instead of the value being written into the
template body at all. That collapses the three files above into one:
// account.json — one template, every tier/itemCount combination
{
"id": "<createSeq('acctId', 'uuid')><getSeq('acctId')>",
"tier": "<getParam('tier', 'free')>",
"itemCount": "<getParam('itemCount', 0)>",
"email": "<getRandomEmail()>"
}
getParam('tier', 'free') returns whatever params.tier was sent on the
request, or falls back to 'free' if the request didn't include it — so
the template still generates a sane default even for a caller that never
mentions tier at all. getParam('itemCount', 0) does the same for the
cart size. The same template body now produces every scenario from the
example above, driven entirely by what you pass in params:
{ "tier": "paid", "itemCount": 5 }
{ "tier": "free", "itemCount": 0 }
Whichever object is sent as params in the generation request decides
the output — the template file itself never changes between scenarios.
Calling it per test case or CI job
Against the persisted-template endpoint, each call's params field picks
the scenario:
curl -s -X POST https://api.jsonfabrica.com/v1/templates/tpl_account/generate \
-H 'Authorization: Bearer sk_...' \
-H 'content-type: application/json' \
-d '{ "params": { "tier": "paid", "itemCount": 5 } }'
curl -s -X POST https://api.jsonfabrica.com/v1/templates/tpl_account/generate \
-H 'Authorization: Bearer sk_...' \
-H 'content-type: application/json' \
-d '{ "params": { "tier": "free", "itemCount": 0 } }'
Wired into a test suite or CI matrix, that's one parameterized job instead of one job per scenario file:
const scenarios = [
{ name: "paid, populated", params: { tier: "paid", itemCount: 5 } },
{ name: "free, empty", params: { tier: "free", itemCount: 0 } },
{ name: "paid, empty cart", params: { tier: "paid", itemCount: 0 } },
];
for (const { name, params } of scenarios) {
test(name, async () => {
const res = await fetch(
"https://api.jsonfabrica.com/v1/templates/tpl_account/generate",
{
method: "POST",
headers: {
Authorization: "Bearer sk_...",
"content-type": "application/json",
},
body: JSON.stringify({ params }),
}
);
const { data: account } = await res.json();
// assertions against `account`
});
}
Adding a fourth scenario — "trial tier, 20 items" — is a new entry in
that array, not a new file. The ad-hoc generate
endpoint works the same way if you're
iterating on the template body itself rather than calling a saved
templateId: pass body and params together in one request while
you're still shaping the template.
Parameterized test fixtures inside a batch
params isn't limited to single-document calls — each document entry in
a batch request carries its own params
field, so one batch can generate a paid account and a free account from
the same template in a single call instead of two separate requests:
{
"seed": 42424242,
"documents": [
{
"templateId": "tpl_account",
"alias": "paidAccount",
"count": 1,
"params": { "tier": "paid", "itemCount": 5 }
},
{
"templateId": "tpl_account",
"alias": "freeAccount",
"count": 1,
"params": { "tier": "free", "itemCount": 0 }
}
]
}
Both documents reference the same templateId; only the params object
per document differs. That's one template covering both halves of a
"compare a paid account's behavior against a free account's" test,
instead of a template per tier plus manual stitching afterward.
Required params vs. optional params with a fallback
getParam has two forms, and picking the right one per field matters.
getParam('tier', 'free') — with a default — never fails: it's the right
call for a field where "just use something reasonable" is an acceptable
fallback when a caller forgets to pass it. getParam('tier') — with no
second argument — has no fallback: if params.tier is missing from the
request, generation throws an error (getParam: parameter 'tier' was not provided and has no default) instead of silently defaulting.
Use the no-default form for params a scenario can never legitimately
omit — a discount template that requires a promoCode param, say, where
generating "a discount for no code in particular" isn't a meaningful
fixture and should fail loudly instead of producing garbage silently.
Use the form with a default for everything where a sane fallback exists,
which is most fields in most templates.
When one template stops being enough
Parameterizing values isn't the same as parameterizing shape. getParam
swaps out what a field contains — a tier string, an item count, a
status flag — not which fields exist or how they're nested. A "valid
order" and an "order missing its required customerId" aren't the same
shape with different values; that's a structural difference, and belongs
in two templates (or a boundary-value template with a literal null, as
covered in composing boundary value test
data), not one template with a
param toggling a field's presence. The rule of thumb: if every scenario
you're collapsing shares the same fields and only the values differ,
getParam turns N templates into one. If the fields themselves differ
between scenarios, keep the templates separate — forcing a structural
difference through a single parameterized template just relocates the
duplication into if/elseIf branches instead of removing it.
FAQ
How do I make a test data template reusable across multiple test
scenarios?
Replace the values that differ between scenarios with getParam(name, default) calls instead of hardcoding them, then pass a different
params object in the generation request for each scenario. One
template body produces a free-tier fixture, a paid-tier fixture, or an
empty-state fixture depending only on what params you send, so you stop
maintaining near-duplicate template files.
What does getParam do in a JsonFabrica template?
getParam(name, default?) reads a value from the params object sent in
the generation request body. If a param with that name was supplied, it
returns that value; otherwise it returns the default you passed as the
second argument, if any. Call it with no default, as getParam('tier'),
and generation throws an error whenever that param is missing, which is
useful for params a scenario must always supply.
What's the difference between getParam with a default and without
one?
getParam('tier', 'free') always succeeds: it returns whatever
params.tier was sent, or falls back to 'free' if the request didn't
include it, so the template still works when a caller forgets that
param. getParam('tier') with no second argument has no fallback — if
params.tier is missing from the request, generation fails with an
error instead of silently defaulting, which is the right choice for a
param a test case can never legitimately omit.
Can different documents in the same batch use different params?
Yes. Each document entry in a batch request's documents array has its
own params field, so one batch can generate a paid-tier account and a
free-tier account from the same account template in a single request,
each with the params that apply to it, instead of running two separate
batch calls.
Should I still keep separate template files for very different
scenarios?
It depends on how much actually differs. If two scenarios share the same
field shape and only differ in a handful of values, like a plan tier, a
quantity, or a status flag, one parameterized template with getParam is
simpler to maintain than two files. If the scenarios need genuinely
different fields or structure, not just different values, separate
templates are still the clearer option — parameterizing values doesn't
mean parameterizing shape.
Reusable, parameterized test data templates are just getParam used
deliberately: one shape, one set of shared fields, and a params object
per call deciding which scenario comes out. Try it against your own
account, order, or user template with the generate
API, passing a different params object
per test case instead of maintaining a template per case.
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.