Boundary Value Test Data Generation: Composing Your Own Edge Cases
Boundary value test data generation is the practice of deliberately
generating the values at the edges of what a field allows — empty strings,
explicit nulls, the largest number a column accepts, the day a year turns
leap — instead of letting a generator hand you the comfortable middle.
Most test data, JsonFabrica's included, defaults to plausible-looking,
statistically typical values on purpose: a getRandomNumber(0, 100) call
clusters around ordinary integers, a getRandomName() call returns an
ordinary name. That's exactly right for everyday fixtures. It's also exactly
where bugs hide from your test suite, because production doesn't only send
you the middle of the distribution — it eventually sends you 0, -1,
"", null, and a string three bytes longer than your column allows.
There's no switch in JsonFabrica that flips a template into "edge case mode." Boundary values are composed, not toggled — a combination of ordinary functions called with extreme arguments, literal fields for values you want pinned exactly, and params for values you want to override per run. This post walks through composing that for strings, numbers, dates, arrays, and nulls, and how to keep the result out of your everyday fixtures.
There's no boundary value test data mode — you compose it yourself
Every one of JsonFabrica's generation functions — getRandomNumber,
getRandomTextWithSpaces, getRandomDate, and the rest of the function
reference — takes ordinary arguments and returns an
ordinary value from the range you give it. None of them has an "extreme"
or "boundary" mode. What makes a value a boundary value is the arguments
you choose to pass, or bypassing the function entirely with a literal.
Three building blocks do all the work:
- Function calls with extreme arguments —
getRandomNumber(-999999, -1)for negative test data,getRandomTextWithSpaces(500, 500)for a max-length string. - Literal fields — a plain JSON value written directly into the
template body with no placeholder at all, for a value you want pinned
exactly the same on every generation:
"",null, or a hand-picked emoji string. - Params —
getParam('age', 0)reads a value supplied on the generation request, with a default if none is given, so the same template can produce a normal value or a pinned boundary value depending on what you pass at run time.
None of these are special to boundary testing — they're the same three mechanisms every JsonFabrica template already uses. Composing edge cases is just choosing where to point them.
Strings: empty, max-length, and unicode
An empty string is the simplest boundary and needs no function call at all — write the field as a literal:
{ "notes": "" }
For a string pinned at an exact maximum length, call getRandomTextWithSpaces
with matching min and max so there's no range to randomize:
{ "bio": "<getRandomTextWithSpaces(500, 500)>" }
That's the closed-form way to hit "exactly the column limit" instead of "somewhere under it." There's no dedicated unicode or emoji function in the catalog, so multi-byte and emoji test data is composed the same way an empty string is — as a literal, since JSON is UTF-8 natively and needs no escaping:
{ "displayName": "Zoë Müller 🎉 日本語" }
Keep a small pool of these literals (accented Latin, CJK, emoji, right-to-left script) in your chaos template rather than trying to derive them from a function — there isn't one, and a hardcoded literal is honest about that.
Negative test data and other numeric boundaries
getRandomNumber(min, max, decimals?) takes whatever bounds you give it,
including negative ones, which is all you need for negative test data:
{
"temperatureCelsius": "<getRandomNumber(-50, -1)>",
"quantity": "<getRandomNumber(0, 0)>",
"priceCents": "<getRandomNumber(0, 999999999)>"
}
getRandomNumber(0, 0) pins exactly zero every time — useful for the
"quantity is zero, does the order total still compute" case. For a single,
always-the-same boundary like -1 rather than a random negative number, a
literal is simpler and clearer than a degenerate function call:
{ "quantity": -1 }
Use function calls with extreme bounds when you want a boundary-region value each run, and literals when the test cares about one specific number.
Dates: leap years and timezone boundaries
getRandomDate(from, to) picks a random point in an ISO-8601 range — good
for realistic fixtures, but not for pinning "did we handle February 29th."
For a specific boundary date, skip the range and write it as a literal:
{
"leapDayOrder": "2024-02-29T23:59:59Z",
"yearBoundary": "2024-12-31T23:59:59Z",
"nextDayUtc": "2025-01-01T00:00:00Z"
}
If you want the boundary date to vary between test runs while still staying
inside a tight window around it, getRandomDate with from and to set a
few seconds apart around the boundary works; for the exact instant, a
literal is the direct way to get it.
Empty and very large arrays
Arrays are built with <for(i, start, end)> / <end_for> loops around a
repeated fragment, not a function — an empty array is just a loop that
never runs, or the literal []:
{ "lineItems": [] }
A large array is the same loop with a high end bound:
{ "lineItems": [<for(i, 0, 199)>{ "sku": "<getRandomTextWithSpaces(4,4)>" },<end_for>] }
Two things to watch: the loop bounds in for(i, start, end) are both
inclusive, so for(i, 0, 199) emits 200 items, not 199 — off-by-one is easy
to get wrong here and worth double-checking against the count you actually
want. And the naive loop above leaves a trailing comma after the last item,
which is invalid JSON; guard it with an if so the comma is only emitted
between items, not after the last one, or trim the rendered output before
you parse it. Either way, a single template is also capped at 10,000 total
loop iterations, so "very large array" for boundary testing means large
enough to hit a real limit in your system under test, not an attempt to
generate an unbounded one.
Explicit nulls, and pinning with params
An explicit null is a literal too — no function returns null on
purpose, so write it directly:
{ "middleName": null, "cancelledAt": null }
For a boundary value you want to override without editing the template —
say, running the same "quantity" field through 0, -1, and a huge number
across three separate CI jobs — read it with getParam and supply the
value on each generation request instead of hardcoding three template
variants:
{ "quantity": "<getParam('quantity', 1)>" }
curl -s -X POST https://api.jsonfabrica.com/v1/templates/generate \
-H 'Authorization: Bearer sk_...' \
-H 'content-type: application/json' \
-d '{ "body": "{ \"quantity\": \"<getParam(\'quantity\', 1)>\" }", "params": { "quantity": -1 } }'
Literals and params solve two different problems: a literal fixes a value for good, a param lets the same template hand you a different boundary on every run without a template edit.
Keep a separate chaos template, not a mixed one
Don't fold null, -1, and max-length strings into the same template you
use for everyday fixtures — a realistic-looking customer record that
occasionally has a null email or a negative order total defeats the point
of realistic fixtures, and it makes ordinary test failures harder to read.
Instead, keep two templates per entity: the normal one with realistic,
middle-of-the-road values for everyday development and demos, and a second
"chaos" or "edge-case" template for the same shape that pins the boundaries
covered above. Generate from the chaos template only when you're
specifically exercising boundary and negative-path behavior, and iterate on
it with the ad-hoc generate endpoint
(POST /v1/templates/generate) before committing it, since it renders a raw
template body without persisting anything.
FAQ
Does JsonFabrica have a built-in edge case or boundary value mode? No. There is no flag or mode that automatically generates boundary values. JsonFabrica's generation functions default to plausible, middle-of-the-road values by design. Boundary and edge case test data is something you compose yourself in a template, using literal fields for fixed values, function calls with extreme arguments, and params for values you want to override at generation time.
How do I generate an empty string as test data?
Write the field as a literal empty string in the template, for example
"notes": "", with no placeholder at all. You can also call
getRandomTextWithSpaces(0, 0), which returns an empty string because its
minLen and maxLen bounds are both zero, but a plain literal is simpler
when you just want a fixed empty value every time.
How do I generate negative or extremely large numbers for testing?
Pass extreme bounds to getRandomNumber. getRandomNumber(-1000000, -1)
generates negative test data, and getRandomNumber(0, 999999999999)
generates numbers near the upper end of what your system should tolerate.
For an exact pinned value like -1 or 0 rather than a random one, use a
literal number field instead of a function call.
How do I test with null or missing fields?
Write the field as a literal JSON null, for example "middleName": null,
directly in the template body. This is not a function output; it's a fixed
literal the same way a hardcoded string or number is, and it renders as an
explicit null in every generated document unless you override it with a
param.
Should edge case test data live in the same template as my normal fixtures? No, keep them separate. Maintain your everyday template with realistic, statistically typical values, and a second "chaos" template for the same entity that pins boundary values instead. Generate the chaos template on demand for boundary and negative-path tests, so extreme values never leak into fixtures meant to look like normal production data.
What is the difference between a literal field and a param for boundary testing?
A literal field is a fixed value baked into the template body itself,
unchanged across every generation call unless you edit the file. A param is
a placeholder read with getParam(name, default) whose value is supplied
per request, so the same template can produce a normal value, a zero, or a
huge number depending on what you pass in that run — without touching the
template.
Boundary value test data generation is a composition exercise, not a checkbox — literals, extreme function arguments, and params are the same three tools JsonFabrica already gives every template, pointed at the edges instead of the middle.
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.