<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Matej Štetiar</title>
    <description>The latest articles on DEV Community by Matej Štetiar (@matejstetiar).</description>
    <link>https://dev.to/matejstetiar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4107710%2F94bc0323-d8d8-4f79-9592-159c184f6ce4.png</url>
      <title>DEV Community: Matej Štetiar</title>
      <link>https://dev.to/matejstetiar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/matejstetiar"/>
    <language>en</language>
    <item>
      <title>Playwright Test Data: Seeding a Real Backend for E2E Suites</title>
      <dc:creator>Matej Štetiar</dc:creator>
      <pubDate>Thu, 03 Sep 2026 09:44:26 +0000</pubDate>
      <link>https://dev.to/matejstetiar/playwright-test-data-seeding-a-real-backend-for-e2e-suites-3ieh</link>
      <guid>https://dev.to/matejstetiar/playwright-test-data-seeding-a-real-backend-for-e2e-suites-3ieh</guid>
      <description>&lt;p&gt;Playwright test data is the set of database rows or API records your&lt;br&gt;
application needs to already contain before a browser test runs against&lt;br&gt;
it — a logged-in user, their orders, the products those orders&lt;br&gt;
reference — generated deterministically so the same run produces the&lt;br&gt;
same data every time. Unlike unit tests, a Playwright (or Cypress) spec&lt;br&gt;
drives a real browser against a real, running app, which means the&lt;br&gt;
backend behind it needs real rows to serve, not an intercepted network&lt;br&gt;
response. Getting that data right, and getting it there before the&lt;br&gt;
first test starts, is most of what makes a browser E2E suite fast and&lt;br&gt;
non-flaky instead of slow and order-dependent.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why is E2E test data hard to manage?
&lt;/h2&gt;

&lt;p&gt;Three patterns keep showing up, and each causes a different failure&lt;br&gt;
mode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tests create their own data through the UI.&lt;/strong&gt; A test that needs an
order to exist first signs up a user, logs in, adds a product to a
cart, and checks out — all before the actual assertion it cares
about. That's slow multiplied across every spec that needs similar
setup, and it means the thing under test (the UI) is also the thing
doing the setup, so a bug in signup breaks fifty unrelated tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A shared, mutable test database.&lt;/strong&gt; If every spec reads and writes
the same rows, test order starts to matter: a test that deletes a
user breaks a later test that assumed that user still exists. This is
one of the most common sources of a suite that passes locally, one
file at a time, and fails intermittently in CI when specs run in
parallel or in a different order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand-maintained fixture SQL or JSON.&lt;/strong&gt; A &lt;code&gt;fixtures.sql&lt;/code&gt; file or a
static &lt;code&gt;users.json&lt;/code&gt; works until the schema changes — a column gets
renamed, a new required field is added — and the fixture silently
stops matching what the app expects, or starts failing inserts with
no clear signal about which of forty rows is the problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix for all three is the same shape: generate the data the suite&lt;br&gt;
needs from a definition (a template), with a fixed seed, right before&lt;br&gt;
the suite runs, and load it in directly rather than through the UI.&lt;/p&gt;
&lt;h2&gt;
  
  
  How do you manage Playwright test data for E2E suites?
&lt;/h2&gt;

&lt;p&gt;The pattern is: describe the records your app needs as templates, fix a&lt;br&gt;
&lt;code&gt;seed&lt;/code&gt; so the same call always produces the same ids and field values,&lt;br&gt;
and generate them as one relational batch so a logged-in test user&lt;br&gt;
actually owns the orders your assertions expect — no separately&lt;br&gt;
generated orders that need their &lt;code&gt;userId&lt;/code&gt; patched in afterward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20260903&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"documents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"templateId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tpl_user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"alias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jane.doe@example.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"templateId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tpl_order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"alias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"relations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user.id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"strategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"round-robin"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Posted to the &lt;a href="https://jsonfabrica.com/docs/api-reference/batches" rel="noopener noreferrer"&gt;batch generation API&lt;/a&gt;, that&lt;br&gt;
returns one user and three orders where every &lt;code&gt;order.userId&lt;/code&gt; is that&lt;br&gt;
user's generated id. Because the &lt;code&gt;seed&lt;/code&gt; is fixed, the user's name,&lt;br&gt;
email, and order details come back identical on every run, which is&lt;br&gt;
what lets a spec assert something as specific as "Jane Doe has exactly&lt;br&gt;
3 orders" instead of a vaguer "the orders list is non-empty." Small&lt;br&gt;
batches return the records synchronously in the response; a large batch&lt;br&gt;
returns a &lt;code&gt;batchId&lt;/code&gt; you poll instead, which matters if a suite seeds&lt;br&gt;
thousands of rows before a load-style E2E run.&lt;/p&gt;

&lt;p&gt;This is a different problem from serving mock API responses. If you're&lt;br&gt;
mocking network calls so a frontend can run with no backend at all, see&lt;br&gt;
&lt;a href="https://jsonfabrica.com/blog/mock-api-response-data" rel="noopener noreferrer"&gt;mock API response data&lt;/a&gt; instead — this&lt;br&gt;
post is about the opposite case, where a real backend and database&lt;br&gt;
exist and the browser test exercises them end to end. It's also&lt;br&gt;
different from seeding a human developer's local database for&lt;br&gt;
onboarding, covered in &lt;a href="https://jsonfabrica.com/blog/seed-local-dev-database-test-data" rel="noopener noreferrer"&gt;seeding a local dev database&lt;/a&gt;;&lt;br&gt;
that data is long-lived and browsed by hand, while E2E test data is&lt;br&gt;
short-lived, created or reset per run or per CI job, and read by&lt;br&gt;
assertions that check exact values.&lt;/p&gt;
&lt;h2&gt;
  
  
  How do I wire test data generation into Playwright's global setup?
&lt;/h2&gt;

&lt;p&gt;Call the batch API from &lt;code&gt;globalSetup&lt;/code&gt;, before any spec file runs, and&lt;br&gt;
save what comes back to a fixture file specs can import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;globalSetup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./e2e/global-setup.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// e2e/global-setup.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;globalSetup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resetTestDatabase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// your own truncate/reset code&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.jsonfabrica.com/v1/batches&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JSONFABRICA_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20260903&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;templateId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tpl_user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;templateId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tpl_order&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;relations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user.id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;round-robin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// results is a flat array of { batchId, alias, seqNo, templateId,&lt;/span&gt;
  &lt;span class="c1"&gt;// status, result, documentSeed }; group by alias for easy lookup.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seeded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seeded&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadIntoDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seeded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// your own insert code, or POST to your API&lt;/span&gt;

  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./e2e/fixtures/seeded-data.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seeded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Specs then import &lt;code&gt;seeded-data.json&lt;/code&gt; instead of re-deriving expected&lt;br&gt;
values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;seeded&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../fixtures/seeded-data.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order history shows all orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;seeded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/orders`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order-row&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toHaveCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;seeded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be clear about what's actually happening here: JsonFabrica returns JSON&lt;br&gt;
records over HTTP. It doesn't insert anything into your database and&lt;br&gt;
it isn't a Playwright plugin, fixture, or reporter — &lt;code&gt;resetTestDatabase&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;loadIntoDatabase&lt;/code&gt; are your own code, the same as they'd be if the&lt;br&gt;
data came from a static fixture file. What changes is where the data&lt;br&gt;
comes from and how it's kept consistent, not who's responsible for&lt;br&gt;
getting it into the app.&lt;/p&gt;

&lt;p&gt;The same generated payload can also back an authenticated&lt;br&gt;
&lt;code&gt;storageState&lt;/code&gt;: seed the user in &lt;code&gt;globalSetup&lt;/code&gt;, log in via an API call&lt;br&gt;
using that user's generated credentials, and save the resulting cookies&lt;br&gt;
so every spec starts already authenticated instead of clicking through&lt;br&gt;
a login form.&lt;/p&gt;

&lt;p&gt;Cypress doesn't have a &lt;code&gt;globalSetup&lt;/code&gt; hook, but the equivalent is a&lt;br&gt;
&lt;code&gt;before()&lt;/code&gt; block in a top-level spec, or a &lt;a href="https://docs.cypress.io/api/commands/task" rel="noopener noreferrer"&gt;&lt;code&gt;cy.task&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
that runs Node code outside the browser sandbox — either one making the&lt;br&gt;
same HTTP call to the batch API before the suite's tests run.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you isolate E2E test data across parallel workers?
&lt;/h2&gt;

&lt;p&gt;Running specs across multiple Playwright workers against a shared&lt;br&gt;
backend means two workers can both try to draw the "next" sequential&lt;br&gt;
order number or invoice ID at the same time. Namespacing avoids the&lt;br&gt;
collision: pass a &lt;code&gt;sequenceNamespace&lt;/code&gt; and &lt;code&gt;variableNamespace&lt;/code&gt; on the&lt;br&gt;
batch request, keyed to the worker index (&lt;code&gt;process.env.TEST_PARALLEL_INDEX&lt;/code&gt;&lt;br&gt;
in Playwright), so each worker gets its own counters instead of sharing&lt;br&gt;
one. The mechanics of how &lt;code&gt;createSeq&lt;/code&gt;/&lt;code&gt;getSeq&lt;/code&gt; and namespaces produce&lt;br&gt;
collision-free values are covered in&lt;br&gt;
&lt;a href="https://jsonfabrica.com/blog/unique-test-data-sequences-counters" rel="noopener noreferrer"&gt;unique test data generation with sequences and counters&lt;/a&gt; —&lt;br&gt;
worth reading if your templates use sequences for order numbers, invoice&lt;br&gt;
numbers, or anything else that has to be unique within a run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should E2E tests use a fixed seed or random data?
&lt;/h2&gt;

&lt;p&gt;Both, at different times. A fixed &lt;code&gt;seed&lt;/code&gt; is the default: it's what&lt;br&gt;
makes an assertion like "the third order in the list is a $42.00&lt;br&gt;
Widget" stable across a hundred CI runs, and it's required for visual&lt;br&gt;
regression snapshots to stay pixel-stable. But a suite that only ever&lt;br&gt;
runs against the same seed can accumulate hidden assumptions — a test&lt;br&gt;
that happens to pass because generated record #2 always sorts after&lt;br&gt;
record #1, not because the sort logic is actually correct. Periodically&lt;br&gt;
run the suite with the seed dropped (or rotated) to let genuinely&lt;br&gt;
different data flow through and catch order-dependence or&lt;br&gt;
off-by-one assumptions that a fixed seed was quietly hiding.&lt;/p&gt;

&lt;p&gt;Whichever mode you're in, schema changes are still a manual step:&lt;br&gt;
adding a required field to the &lt;code&gt;orders&lt;/code&gt; table means editing the&lt;br&gt;
&lt;code&gt;tpl_order&lt;/code&gt; template by hand, the same way you'd edit a fixture file —&lt;br&gt;
generation doesn't watch your schema for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I seed test data for Playwright tests?&lt;/strong&gt;&lt;br&gt;
Call a batch generation API from &lt;code&gt;playwright.config.ts&lt;/code&gt;'s &lt;code&gt;globalSetup&lt;/code&gt;,&lt;br&gt;
before any spec runs, and write the created records (with their&lt;br&gt;
generated ids) to a JSON fixture file that your specs import. Reset the&lt;br&gt;
backend to a known empty or baseline state first, then insert the&lt;br&gt;
generated batch, so every run starts from the same data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should Playwright tests create their own data through the UI?&lt;/strong&gt;&lt;br&gt;
Not as the default pattern. Clicking through signup and creation flows&lt;br&gt;
to set up state for every test is slow, and it makes tests&lt;br&gt;
order-dependent when they share a database, which is a common source of&lt;br&gt;
flaky suites. Reserve UI-driven creation for the handful of tests&lt;br&gt;
actually verifying that creation flow, and seed everything else&lt;br&gt;
directly through an API before the suite starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Playwright global setup used for?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;globalSetup&lt;/code&gt; is a function Playwright runs once before any test file,&lt;br&gt;
configured via the &lt;code&gt;globalSetup&lt;/code&gt; option in &lt;code&gt;playwright.config.ts&lt;/code&gt;. It's&lt;br&gt;
the standard place to seed a backend's database, generate an&lt;br&gt;
authenticated &lt;code&gt;storageState&lt;/code&gt;, or do any other one-time preparation the&lt;br&gt;
whole suite depends on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you keep parallel E2E test workers from colliding on data?&lt;/strong&gt;&lt;br&gt;
Give each parallel worker its own namespace when generating data, so&lt;br&gt;
counters and any workflow-scoped state don't overlap between workers&lt;br&gt;
hitting the same backend at once. JsonFabrica's batch API exposes this&lt;br&gt;
as a &lt;code&gt;sequenceNamespace&lt;/code&gt; and &lt;code&gt;variableNamespace&lt;/code&gt; per request, keyed on&lt;br&gt;
something like the worker index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does JsonFabrica integrate directly with Playwright or Cypress?&lt;/strong&gt;&lt;br&gt;
No. There's no official plugin, fixture, or reporter for either tool.&lt;br&gt;
JsonFabrica is an HTTP API that returns JSON records; you call it from&lt;br&gt;
your own &lt;code&gt;globalSetup&lt;/code&gt;, &lt;code&gt;before()&lt;/code&gt; hook, or &lt;code&gt;cy.task&lt;/code&gt;, and you write the&lt;br&gt;
code that loads the returned records into your application's database&lt;br&gt;
or API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should E2E tests use a fixed seed or random data?&lt;/strong&gt;&lt;br&gt;
Use a fixed seed most of the time, because it makes generated ids and&lt;br&gt;
field values identical across runs, which is what lets an assertion&lt;br&gt;
like "Jane Doe has exactly 3 orders" stay true instead of drifting.&lt;br&gt;
Periodically run the suite with the seed dropped so genuinely random&lt;br&gt;
data can surface assertions that were quietly depending on a specific&lt;br&gt;
generated value instead of the behavior you meant to test.&lt;/p&gt;

&lt;p&gt;Deterministic, relational test data is what the &lt;a href="https://jsonfabrica.com/docs/api-reference/batches" rel="noopener noreferrer"&gt;batch generation&lt;br&gt;
API&lt;/a&gt; is built for — seed it once in your&lt;br&gt;
&lt;code&gt;globalSetup&lt;/code&gt;, and every Playwright or Cypress run gets the same known&lt;br&gt;
data to assert against.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>AI Agent Test Data Generation via MCP Server</title>
      <dc:creator>Matej Štetiar</dc:creator>
      <pubDate>Thu, 03 Sep 2026 09:44:15 +0000</pubDate>
      <link>https://dev.to/matejstetiar/ai-agent-test-data-generation-via-mcp-server-10h0</link>
      <guid>https://dev.to/matejstetiar/ai-agent-test-data-generation-via-mcp-server-10h0</guid>
      <description>&lt;p&gt;An AI coding agent working inside Claude Desktop or Cursor can read your&lt;br&gt;
code, write new files, and run your test suite — but it can't open a&lt;br&gt;
browser, log into a dashboard, and click "generate" to get a batch of&lt;br&gt;
realistic test data. It has no hands for a UI. AI agent test data&lt;br&gt;
generation only works if there's something the agent can &lt;em&gt;call&lt;/em&gt;: a tool&lt;br&gt;
with a defined schema it can invoke mid-session, the same way it calls a&lt;br&gt;
file-write or a shell command. That's exactly what the Model Context&lt;br&gt;
Protocol (MCP) is for, and it's why we shipped&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/@jsonfabrica/mcp-server" rel="noopener noreferrer"&gt;&lt;code&gt;@jsonfabrica/mcp-server&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
on npm.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI agent test data generation requires over MCP
&lt;/h2&gt;

&lt;p&gt;MCP lets an AI client — Claude Desktop, Cursor, or anything else that&lt;br&gt;
speaks the protocol — launch a small local server over stdio and treat&lt;br&gt;
its exposed functions as tools it can call during a conversation. The&lt;br&gt;
agent decides when to call &lt;code&gt;jsonfabrica_generate_from_template&lt;/code&gt; the same&lt;br&gt;
way it decides when to call &lt;code&gt;read_file&lt;/code&gt;. For that to work, three things&lt;br&gt;
have to exist: a server process the client can start, a set of tool&lt;br&gt;
definitions with typed inputs and outputs, and — underneath all of it —&lt;br&gt;
some actual operation the tool call triggers. MCP server test data&lt;br&gt;
generation is that last piece: the tool call has to result in real,&lt;br&gt;
schema-conformant data coming back, not a stub.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;@jsonfabrica/mcp-server&lt;/code&gt;, concretely
&lt;/h2&gt;

&lt;p&gt;We published &lt;code&gt;@jsonfabrica/mcp-server&lt;/code&gt; v0.1.1 as a local MCP server: the&lt;br&gt;
AI client launches it itself over stdio, no separate process to manage,&lt;br&gt;
no port to open. It exposes the JsonFabrica gateway as a set of MCP&lt;br&gt;
tools — &lt;code&gt;jsonfabrica_create_template&lt;/code&gt;, &lt;code&gt;jsonfabrica_generate_from_template&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;jsonfabrica_generate_adhoc&lt;/code&gt;, &lt;code&gt;jsonfabrica_create_batch&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;jsonfabrica_create_sequence&lt;/code&gt;, and more. Mid-session, an agent can create&lt;br&gt;
a template matching the shape of your &lt;code&gt;User&lt;/code&gt; or &lt;code&gt;Order&lt;/code&gt; model, generate&lt;br&gt;
a batch of realistic records against it, and drop the result straight&lt;br&gt;
into a fixture file or a seed script — without you leaving the editor to&lt;br&gt;
go configure anything by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this required building nothing new
&lt;/h2&gt;

&lt;p&gt;Here's the part worth being explicit about: every one of those MCP&lt;br&gt;
tools is a thin, typed wrapper around an endpoint that already existed&lt;br&gt;
in the JsonFabrica REST API. &lt;code&gt;jsonfabrica_generate_from_template&lt;/code&gt; calls&lt;br&gt;
the same generation endpoint a CI pipeline or a seed script would call.&lt;br&gt;
Writing the MCP server was a matter of describing existing requests and&lt;br&gt;
responses as tool schemas — input validation, output shape, a short&lt;br&gt;
description for the model to read — not building new generation logic,&lt;br&gt;
new data models, or a new backend. The API was already the product; the&lt;br&gt;
MCP server just gives it a second front door.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a UI-first tool would have had to wrap instead
&lt;/h2&gt;

&lt;p&gt;Contrast that with a tool where the primary interface is a dashboard:&lt;br&gt;
form fields, dropdowns, a "generate" button wired to internal state that&lt;br&gt;
was never meant to be called from outside a browser session. Exposing&lt;br&gt;
that to an AI agent means building an API it never had — endpoints,&lt;br&gt;
request validation, auth, versioned responses — essentially rebuilding&lt;br&gt;
the product's backend to have something to wrap. AI agent test data&lt;br&gt;
generation isn't a feature you bolt onto a UI-first product after the&lt;br&gt;
fact; it's a natural consequence of the product being API-first from the&lt;br&gt;
start. If the REST API is solid, wrapping it for MCP is a week of typed&lt;br&gt;
schemas. If it isn't, MCP support means building the API you should have&lt;br&gt;
had all along.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I connect JsonFabrica to Claude Desktop or Cursor?&lt;/strong&gt;&lt;br&gt;
Install &lt;code&gt;@jsonfabrica/mcp-server&lt;/code&gt; from npm and add it as an MCP server in&lt;br&gt;
your client's config. The client launches the server itself over stdio, so&lt;br&gt;
there's no separate process to run or port to open, and the agent can then&lt;br&gt;
call its tools directly in a session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is an MCP server, and why does it matter for AI coding agents?&lt;/strong&gt;&lt;br&gt;
MCP, the Model Context Protocol, lets an AI client like Claude Desktop or&lt;br&gt;
Cursor launch a small local server and treat its exposed functions as&lt;br&gt;
tools it can call mid-conversation, the same way it calls a file-write or&lt;br&gt;
a shell command. Without it, an agent has no way to invoke an external&lt;br&gt;
service like a test data API, since it can't open a browser and click&lt;br&gt;
through a UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can an AI agent generate relational or batch test data through MCP, not just single records?&lt;/strong&gt;&lt;br&gt;
Yes — &lt;code&gt;jsonfabrica_create_batch&lt;/code&gt; wraps the same batch generation endpoint&lt;br&gt;
the REST API and CI pipelines use, so an agent can generate a customer and&lt;br&gt;
a set of linked orders in one call during a coding session, not just&lt;br&gt;
isolated single documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does JsonFabrica's MCP server require a separate backend from the REST API?&lt;/strong&gt;&lt;br&gt;
No. Every MCP tool, such as &lt;code&gt;jsonfabrica_generate_from_template&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;jsonfabrica_create_sequence&lt;/code&gt;, is a thin typed wrapper around an endpoint&lt;br&gt;
that already exists in the JsonFabrica REST API — there's no separate&lt;br&gt;
generation logic or data model behind the MCP server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually changes in a coding session
&lt;/h2&gt;

&lt;p&gt;In practice, it collapses a context switch. Instead of stopping to write&lt;br&gt;
a one-off fixture by hand, or tabbing to a dashboard to generate a CSV&lt;br&gt;
and importing it back, an agent working on a PR can generate the test&lt;br&gt;
data it needs — realistic, schema-conformant, matching the model it's&lt;br&gt;
currently editing — as part of the same conversation that's writing the&lt;br&gt;
tests. No manual step, no separate tool, no copy-pasting JSON between&lt;br&gt;
windows. That's the practical payoff of MCP server test data generation:&lt;br&gt;
not a new capability bolted onto the model, but an existing capability&lt;br&gt;
finally reachable from where the work is actually happening.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>JsonFabrica vs. Mockaroo vs. Faker.js for Test Data Generation</title>
      <dc:creator>Matej Štetiar</dc:creator>
      <pubDate>Thu, 03 Sep 2026 09:43:56 +0000</pubDate>
      <link>https://dev.to/matejstetiar/jsonfabrica-vs-mockaroo-vs-fakerjs-for-test-data-generation-2hj3</link>
      <guid>https://dev.to/matejstetiar/jsonfabrica-vs-mockaroo-vs-fakerjs-for-test-data-generation-2hj3</guid>
      <description>&lt;p&gt;If you're generating test data today, you've probably landed on one of&lt;br&gt;
three approaches: click through a UI like Mockaroo, pull in a library&lt;br&gt;
like Faker.js and write generation code yourself, or call a hosted API&lt;br&gt;
like JsonFabrica. Comparing these test data generation tools side by&lt;br&gt;
side, the real differences aren't about which one produces "better"&lt;br&gt;
fake data — Faker.js, Mockaroo, and JsonFabrica are all capable of&lt;br&gt;
that. The differences are about where the tool lives, how it handles&lt;br&gt;
relationships between records, and who's responsible for running it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three test data generation tools compared, shape by shape
&lt;/h2&gt;

&lt;p&gt;Mockaroo is a browser-based UI: you define columns and types through a&lt;br&gt;
web form, preview rows, and export a file — or hit its API directly,&lt;br&gt;
which is available even on the free tier (paid tiers raise the volume&lt;br&gt;
ceiling rather than gate API access itself). Faker.js is a JavaScript&lt;br&gt;
library: you &lt;code&gt;import&lt;/code&gt; it into your own code and call functions like&lt;br&gt;
&lt;code&gt;faker.person.fullName()&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;faker.internet.email()&lt;/code&gt; to build up objects yourself, one field at a&lt;br&gt;
time. JsonFabrica is an API-first hosted service: you send a schema (or&lt;br&gt;
use a template) to an endpoint and get structured, schema-conformant&lt;br&gt;
JSON back, with no UI step and no library to install in your own&lt;br&gt;
codebase.&lt;/p&gt;

&lt;p&gt;That distinction matters more than it sounds. A UI tool is something a&lt;br&gt;
person operates by hand. A library is something a developer owns and&lt;br&gt;
maintains inside their own project — you write the loops, the&lt;br&gt;
relationships, the edge cases. An API-first tool is infrastructure:&lt;br&gt;
something your CI pipeline, your seed script, or an AI coding agent can&lt;br&gt;
call directly, without a human in the loop or generation logic living&lt;br&gt;
in your repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  UI vs. library vs. API, in practice
&lt;/h2&gt;

&lt;p&gt;Mockaroo's UI is genuinely fast for a one-off task — sketch a schema,&lt;br&gt;
click generate, download a CSV or JSON file. What it isn't built for is&lt;br&gt;
wiring generation into an automated pipeline where nobody is clicking&lt;br&gt;
anything. Its API can cover that, but at free-tier volumes (200&lt;br&gt;
requests/day, up to 1,000 rows per call) it's still built around the&lt;br&gt;
UI-first workflow — for heavier automated use, you either pay for more&lt;br&gt;
API volume or reach for a Mockaroo alternative built API-first from the&lt;br&gt;
start.&lt;/p&gt;

&lt;p&gt;Faker.js sits at the opposite end. It's a library, not a service, so&lt;br&gt;
there's no hosting, no account, and no network call — you generate data&lt;br&gt;
in-process, in whatever language your project already uses (there are&lt;br&gt;
Faker ports for several languages — the original Faker was a Perl&lt;br&gt;
library, later ported to Ruby, PHP, Python, Java, and others; Faker.js&lt;br&gt;
is itself one of those ports, not the original). The tradeoff is that&lt;br&gt;
Faker.js gives you field generators, not a data model. If you want an order that references a&lt;br&gt;
real customer ID, or a set of line items that sum to the order total,&lt;br&gt;
you write that logic yourself, by hand, in every project that needs it.&lt;/p&gt;

&lt;p&gt;JsonFabrica's tradeoff is the mirror image: you don't write generation&lt;br&gt;
logic, because the schema (or a prebuilt template) describes what you&lt;br&gt;
want and the service produces it, relationships and all. What you give&lt;br&gt;
up is the "no dependency" simplicity of a library — you're calling a&lt;br&gt;
hosted API instead of importing a package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema and relational data support
&lt;/h2&gt;

&lt;p&gt;This is where the three tools diverge most. Faker.js has no concept of&lt;br&gt;
a schema or a relationship between generated objects — it's a toolbox&lt;br&gt;
of individual field generators, and any structure across records&lt;br&gt;
(foreign keys, consistent totals, matching timestamps) is code you&lt;br&gt;
write and maintain. Mockaroo lets you define a schema per dataset and&lt;br&gt;
has some support for relating fields within a single generation, but&lt;br&gt;
it's still fundamentally a spreadsheet-shaped tool: one flat schema,&lt;br&gt;
one export, one dataset at a time.&lt;/p&gt;

&lt;p&gt;JsonFabrica is schema-driven and template-based generation by design,&lt;br&gt;
not an add-on. You describe the shape of a record — or reference a&lt;br&gt;
template — and the engine fills it in against real-world patterns&lt;br&gt;
rather than pure randomness, so an order's &lt;code&gt;shipped_at&lt;/code&gt; comes after its&lt;br&gt;
&lt;code&gt;created_at&lt;/code&gt;, and a &lt;code&gt;discount_code&lt;/code&gt; points at a code that actually&lt;br&gt;
exists. Referential integrity across related records — customers that&lt;br&gt;
own orders, orders that own line items — is handled by the generation&lt;br&gt;
engine itself, not left to your seed script to stitch together after&lt;br&gt;
the fact.&lt;/p&gt;

&lt;p&gt;JsonFabrica also has stateful sequence functions (&lt;code&gt;createSeq&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;getSeq&lt;/code&gt;) for cases where you need collision-free unique IDs or&lt;br&gt;
incrementing values across separate test runs — something neither a&lt;br&gt;
stateless library like Faker.js nor a per-export UI tool like Mockaroo&lt;br&gt;
is built to track between calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosted vs. self-run
&lt;/h2&gt;

&lt;p&gt;Faker.js runs entirely inside your own process — no network dependency,&lt;br&gt;
no data leaving your machine, no bill. That's a real advantage if&lt;br&gt;
you want full control and zero external dependencies for a CI job. The&lt;br&gt;
cost is that you own everything: version upgrades, keeping generation&lt;br&gt;
logic consistent across projects, and building relational logic&lt;br&gt;
yourself if you need it.&lt;/p&gt;

&lt;p&gt;Mockaroo is hosted, but its primary interface is the UI; its API is&lt;br&gt;
available free, and paid tiers raise how much you can pull through it&lt;br&gt;
rather than unlocking API access itself. JsonFabrica is hosted and&lt;br&gt;
API-first from the ground up — every feature the UI could offer is available as&lt;br&gt;
an HTTP call, described in a published OpenAPI spec, so it fits into a&lt;br&gt;
CI pipeline or seed script the same way any other API dependency does.&lt;br&gt;
JsonFabrica also ships an MCP server, so AI coding agents in tools like&lt;br&gt;
Claude Desktop or Cursor can request schema-conformant test data&lt;br&gt;
directly in a session, without a person switching to a browser tab.&lt;/p&gt;

&lt;p&gt;The tradeoff for going hosted is the one you'd expect: you depend on an&lt;br&gt;
external service being up, and usage is metered — JsonFabrica bills on&lt;br&gt;
a credit-based model rather than a flat license, so cost scales with&lt;br&gt;
how much data you actually generate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking between them
&lt;/h2&gt;

&lt;p&gt;If you need a quick, one-off dataset and don't mind a manual export, a&lt;br&gt;
UI-based tool like Mockaroo is fast and low-friction. If you want&lt;br&gt;
individual fake field values inside code you already control, and&lt;br&gt;
you're fine writing your own relational logic, Faker.js is a solid,&lt;br&gt;
zero-dependency Faker.js alternative — arguably the default choice for&lt;br&gt;
in-process unit tests. If you need schema-conformant, relationally&lt;br&gt;
consistent test data as part of an automated pipeline, seed script, or&lt;br&gt;
AI agent workflow — without hand-writing the logic that keeps records&lt;br&gt;
consistent with each other — that's the gap JsonFabrica is built to&lt;br&gt;
fill: an API-first, schema-driven service with referential integrity&lt;br&gt;
and sequence functions built in, rather than bolted on after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Mockaroo free to use?&lt;/strong&gt;&lt;br&gt;
Yes — Mockaroo's free tier covers the UI and direct API access, capped at&lt;br&gt;
200 requests per day and up to 1,000 rows per call. Paid tiers raise those&lt;br&gt;
volume limits rather than unlocking API access itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Faker.js the original Faker library?&lt;/strong&gt;&lt;br&gt;
No. The original Faker was a Perl library, later ported to Ruby, PHP,&lt;br&gt;
Python, Java, and other languages. Faker.js is the JavaScript port of that&lt;br&gt;
same lineage, not the original implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can Faker.js generate related records, like an order linked to a real customer ID?&lt;/strong&gt;&lt;br&gt;
Not by itself. Faker.js is a toolbox of individual field generators with no&lt;br&gt;
concept of a schema or a relationship between generated objects — wiring a&lt;br&gt;
foreign key or keeping totals consistent across records is code you write&lt;br&gt;
and maintain yourself, which is exactly the gap schema-driven tools like&lt;br&gt;
JsonFabrica close.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does JsonFabrica have a free plan or trial?&lt;/strong&gt;&lt;br&gt;
JsonFabrica's &lt;a href="https://jsonfabrica.com/pricing" rel="noopener noreferrer"&gt;Starter plan&lt;/a&gt; includes a 30-day free trial. A&lt;br&gt;
valid payment method is required at signup, but you're not charged until&lt;br&gt;
the trial converts; Growth and Business plans bill immediately with no&lt;br&gt;
trial.&lt;/p&gt;

&lt;p&gt;None of these tools are strictly better than the others across the&lt;br&gt;
board. They're built for different points in the workflow, and the&lt;br&gt;
right choice depends on whether you're clicking through a one-off&lt;br&gt;
export, writing generation code by hand, or wiring test data into a&lt;br&gt;
pipeline that has to run without anyone watching it.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Why API-First Wins for Test Data Generation</title>
      <dc:creator>Matej Štetiar</dc:creator>
      <pubDate>Thu, 03 Sep 2026 09:43:38 +0000</pubDate>
      <link>https://dev.to/matejstetiar/why-api-first-wins-for-test-data-generation-aja</link>
      <guid>https://dev.to/matejstetiar/why-api-first-wins-for-test-data-generation-aja</guid>
      <description>&lt;p&gt;Plenty of test data tools are built as a UI first and an API second, if&lt;br&gt;
there's an API at all. You open a dashboard, configure some fields,&lt;br&gt;
click "generate," and download a file. That works fine for a one-off&lt;br&gt;
demo. It falls apart the moment test data generation needs to be part of&lt;br&gt;
your actual engineering workflow — running in CI, seeding a database on&lt;br&gt;
every branch, or producing ten thousand records instead of ten. That's&lt;br&gt;
the case for a test data generation API over a click-driven dashboard:&lt;br&gt;
the primary interface is a request you can make from code, and&lt;br&gt;
everything else — a UI, a CLI — is built on top of that same API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automation and CI integration
&lt;/h2&gt;

&lt;p&gt;A UI is something a person operates. CI doesn't have a person sitting at&lt;br&gt;
it. If test data generation only exists behind a login screen and a&lt;br&gt;
click, it can't run as a step in your pipeline — someone has to&lt;br&gt;
generate the data ahead of time, commit it, and hope it doesn't drift&lt;br&gt;
from what the tests actually need. An API-first tool is just another&lt;br&gt;
HTTP call your pipeline makes: fetch fresh, schema-conformant data as&lt;br&gt;
part of the build, every run, with no manual step in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scriptability — no clicking required
&lt;/h2&gt;

&lt;p&gt;Generating test data through a UI means clicking through the same&lt;br&gt;
sequence of dropdowns and fields every time you need a new batch. That's&lt;br&gt;
tedious for one dataset and untenable for the dozens of shapes a real&lt;br&gt;
test suite needs — different entity types, different edge cases,&lt;br&gt;
different volumes. An API call is a script. Write it once, parametrize&lt;br&gt;
it, and reuse it for every collection you need, without a human&lt;br&gt;
repeating the same clicks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring a test data generation API into pipelines and seed scripts
&lt;/h2&gt;

&lt;p&gt;Seed scripts are code that runs at a specific point in a workflow —&lt;br&gt;
before a test suite, on container startup, in a migration. They need a&lt;br&gt;
function call or an HTTP request they can invoke programmatically, not&lt;br&gt;
a browser tab. With a test data generation API, "seed the dev database&lt;br&gt;
with realistic orders" is a line in a setup script, not a manual task&lt;br&gt;
someone has to remember to do before each demo or test run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Language and framework agnostic
&lt;/h2&gt;

&lt;p&gt;A UI locks you into whatever the tool's frontend supports. An HTTP API&lt;br&gt;
doesn't care what you're writing — a Python test suite, a Go seed&lt;br&gt;
script, a Node.js CI job, or a shell script with &lt;code&gt;curl&lt;/code&gt;, can all call&lt;br&gt;
the same endpoint the same way. JsonFabrica is built this way&lt;br&gt;
deliberately: the API is the product, so it works identically whether&lt;br&gt;
you're calling it from a Jest test, a Django management command, or a&lt;br&gt;
Makefile target. You're not choosing a data tool that happens to support&lt;br&gt;
your stack; you're using a data tool that has no opinion about your&lt;br&gt;
stack at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Versionable, reviewable requests
&lt;/h2&gt;

&lt;p&gt;A UI configuration lives in a database somewhere, edited by clicking&lt;br&gt;
through screens, with no diff and no history beyond an audit log if&lt;br&gt;
you're lucky. An API request is a payload — JSON, typically — that you&lt;br&gt;
can commit to a repo, put next to your test suite, and review in a pull&lt;br&gt;
request like any other code change. When someone adjusts the shape of&lt;br&gt;
generated test data, that change shows up as a diff, gets reviewed, and&lt;br&gt;
has a commit message explaining why. Config-as-code isn't a nice-to-have&lt;br&gt;
here, it's what makes test data changes auditable instead of invisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling to bulk and synthetic data needs
&lt;/h2&gt;

&lt;p&gt;Clicking "generate" in a UI is fine for ten records. It is not how&lt;br&gt;
anyone produces the ten thousand records needed to load-test a database&lt;br&gt;
or populate a staging environment with realistic volume. An API call&lt;br&gt;
takes a &lt;code&gt;count&lt;/code&gt; parameter. Generating 50,000 records programmatically is&lt;br&gt;
the same amount of effort as generating 50 — one request, one number&lt;br&gt;
changed. That gap is exactly where UI-only and template-only tools&lt;br&gt;
without an API stop being useful, and it's exactly where an API-first&lt;br&gt;
tool keeps working.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is a test data generation API, and how is it different from a UI tool?&lt;/strong&gt;&lt;br&gt;
A test data generation API is an HTTP endpoint your code calls to get&lt;br&gt;
structured, schema-conformant data back, instead of a dashboard a person&lt;br&gt;
clicks through. That makes it callable from CI pipelines, seed scripts, and&lt;br&gt;
AI agents without a human in the loop, which a UI-only tool can't support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I call a test data generation API with curl or from any language?&lt;/strong&gt;&lt;br&gt;
Yes — it's a plain HTTP endpoint, so anything that can make an HTTP&lt;br&gt;
request, including curl, a Python test suite, a Go seed script, or a&lt;br&gt;
Node.js CI job, can call it the same way, with no client library or&lt;br&gt;
dashboard required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I generate bulk test data instead of a handful of records?&lt;/strong&gt;&lt;br&gt;
Pass a &lt;code&gt;count&lt;/code&gt; parameter on the generation request. Generating 50,000&lt;br&gt;
records programmatically is the same amount of effort as generating 50 —&lt;br&gt;
one request, one number changed, instead of clicking "generate" repeatedly&lt;br&gt;
in a UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run test data generation as part of a CI pipeline?&lt;/strong&gt;&lt;br&gt;
Yes — that's the core use case for an API-first tool. Because the&lt;br&gt;
generation call is just an HTTP request, it can run as a step in a CI&lt;br&gt;
pipeline or a container startup script, fetching fresh, schema-conformant&lt;br&gt;
data on every run with no manual export step.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A UI is a convenience layer, and a good one is worth having. But it&lt;br&gt;
can't be the foundation, because automation, CI, seed scripts, and bulk&lt;br&gt;
generation all need something a browser click can't provide: a&lt;br&gt;
programmatic, scriptable, versionable interface. Build the API first and&lt;br&gt;
the UI can sit on top of it. Build the UI first and the API — if it&lt;br&gt;
ever arrives — is usually an afterthought that doesn't cover everything&lt;br&gt;
the UI does. JsonFabrica is built API-first for exactly this reason:&lt;br&gt;
structured, schema-conformant data, generated by a request you can&lt;br&gt;
script, version, and run anywhere.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>api</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
