How to Prompt AI to Generate Realistic Test Data
Three worked prompt templates for generating realistic test data with AI: relational rows with constraints, deliberate edge cases, and PII-safe fake records, each with actual output.
The fastest way to get bad test data from AI is to ask for "some test data." You get John Doe, jane@example.com, and IDs that count 1, 2, 3. None of that exercises anything real. Learning how to prompt AI to generate realistic test data means being as specific about the data as you would be about a database schema: exact types, valid ranges, relationships between tables, and the weird inputs that break code in production. Below are three prompt templates for three distinct data shapes, each with the actual output it produced, plus the details that keep AI honoring constraints instead of drifting.
One distinction up front: this is about generating test data, not writing the test code itself. Those are separate problems. A well-written test with garbage fixtures still tells you nothing, and realistic fixtures dropped into a shallow test are wasted.
Why "generate some test data" prompts fail
A vague prompt gives the model nothing to anchor on, so it falls back to whatever completions are most statistically common: sequential integers for IDs, the same handful of names (there is a reason so much sample data features someone named Sarah or a company called Acme), emails at example.com, and rows with no relationship to each other. None of that resembles the mess a real database accumulates: skewed distributions, foreign keys that must actually match, timestamps that have to be in the right order, and the occasional record that is technically valid but structurally weird.
The fix is not a magic phrase. It is giving the model the same constraints you would put in a schema, a validator, or a code review, and asking for three different shapes of data depending on what you are actually testing.
1. Relational rows with realistic constraints
For integration tests and staging seeds, the data needs referential integrity: foreign keys that resolve, values that respect each row's own state, and dates that happen in a sane order. Naming the fields and their rules explicitly is what makes this work; leaving any of it implicit is where the drift creeps in.
Generate 4 rows of test data for a `users` table and 6 rows for an `orders` table that references it.
users: id, full_name, email, signup_date (between 2023-01-01 and 2024-06-30), plan (free/pro/team, weighted roughly 60/30/10), country (2-letter ISO code, mostly US/GB/DE/IN)
orders: id, user_id (must match an existing users.id), amount_usd (free = 0.00, pro = 9-49, team = 49-299), status (paid/refunded/failed, paid about 85% of the time), created_at (must be after that user's signup_date)
Output as two markdown tables. No placeholder text like "example" or "test" in any field.Here is what that actually produced:
id | full_name | signup_date | plan | country | |
|---|---|---|---|---|---|
u1 | Priya Chandrasekaran | priya.chandra88@gmail.com | 2023-02-14 | pro | IN |
u2 | Marcus Webb | m.webb@outlook.com | 2023-07-01 | free | GB |
u3 | Sofia Alvarez | sofia.alvarez.dev@proton.me | 2023-11-22 | team | DE |
u4 | Devon Okafor | devon.okafor@yahoo.com | 2024-03-05 | free | US |
id | user_id | amount_usd | status | created_at |
|---|---|---|---|---|
o1 | u1 | 29.00 | paid | 2023-03-01 |
o2 | u1 | 29.00 | paid | 2023-06-14 |
o3 | u2 | 0.00 | paid | 2023-07-05 |
o4 | u3 | 149.00 | paid | 2023-12-01 |
o5 | u3 | 89.00 | refunded | 2024-01-15 |
o6 | u4 | 0.00 | failed | 2024-03-10 |
Check the constraints against the output and they hold: every order's created_at falls after that user's signup_date, every amount sits inside the range for that user's plan, and every user_id resolves to a real row in the first table. That is the part a vague prompt never gets right, because nothing told the model those relationships mattered.
If you're building this schema description from scratch, giving the model one fully worked example row before asking for the rest is a form of few-shot prompting, and it tends to lock in formatting (date style, decimal places, casing) more reliably than a text description alone. Once the small sample checks out, don't ask for 5,000 rows in the same chat. Ask for a script instead, Python with Faker or a SQL statement using generate_series, seeded with the same rules. Models lose consistency and start repeating rows well before four-digit row counts in a single completion.
2. Deliberately weird edge cases
Realistic data alone will not find your bugs. Most bugs live at the edges: empty strings, nulls, values at a boundary, characters outside the ASCII range, inputs technically valid but structurally strange. Ask for those directly and label what each row is testing so you know why it exists later.
Generate 10 rows for a `product_name` and `price` field pair, each one designed to break a naive parser or validator. Include: a name with a hyphen and an apostrophe, a name in Japanese, a name that's just whitespace, a price of exactly 0, a negative price, a price with 3 decimal places, a name over 200 characters, a null price, a name containing a comma and a quote mark, and an emoji in the name. Label each row with the edge case it targets.product_name | price | edge case targeted |
|---|---|---|
Kate's Wireless Mouse - Pro | 24.99 | apostrophe + hyphen |
ワイヤレスマウス | 3200 | non-Latin unicode |
| 15.00 | whitespace-only name |
Bulk Cable Ties (500-pack) | 0.00 | zero price |
Return Credit Adjustment | -12.50 | negative price |
Ultra HD Cable 2m | 19.999 | price with 3 decimals |
Premium Extended Warranty Plan for All Home Office Electronics Including Laptops, Monitors, and Peripherals Purchased Within the Last Ninety Days… | 49.00 | name over 200 chars |
Refurbished Laptop Stand | null | null price |
Widget, "Deluxe" Edition | 9.99 | embedded comma + quote |
🔥 Flash Sale Charger 🔥 | 14.50 | emoji in name |
Each row maps to a real failure mode: the whitespace-only name breaks a naive required check that only tests truthiness, the comma-and-quote name breaks CSV export if fields aren't quoted properly, and the 3-decimal price breaks any code that assumes money is always two decimals. Feed this table straight into a parameterized test and you've covered ten failure modes with one prompt instead of guessing at them one at a time.
3. PII-safe fake data that still looks real
Support tickets, CRM records, onboarding forms: a lot of test scenarios need name, email, and phone fields that look like a real person filled them out, without being a real person. The trap here is prompting the model to make it "realistic like our actual customers," which pushes it toward generic patterns anyway, or worse, tempts you to paste in real customer rows as a seed. Don't do that, even in a prompt you think is private. Describe the shape you want instead of the source you want it copied from.
Generate 5 rows of fake customer support ticket data for a SaaS app: name, email, phone, and ticket body. None of the values should be derived from or resemble any real person, existing fixture, or public dataset. Use realistic name/email conventions (first initial + last name, common providers) rather than obviously fake patterns like "Test User" or "user@example.com". Vary phone formats by country code, mixing US, UK, and India. Keep ticket bodies under 40 words and specific to a billing or login problem.name | phone | ticket body | |
|---|---|---|---|
R. Castellano | rcastellano82@gmail.com | +1 (312) 555-0148 | Card was charged twice for the pro plan renewal on the 3rd. Can someone refund the duplicate charge? Order id ends in 4471. |
A. Whitfield | a.whitfield@btinternet.com | +44 7700 900312 | Locked out after the password reset email never arrived. Checked spam folder twice. Team plan, three seats. |
N. Deshmukh | n.deshmukh19@yahoo.co.in | +91 98765 43210 | Invoice shows GST calculated on the pre-discount amount instead of after. Can you correct and resend? |
J. Okonkwo | jokonkwo.work@gmail.com | +1 (646) 555-0117 | Downgraded from team to free last week but was still billed the full team amount yesterday. |
M. Sorensen | msorensen@outlook.com | +44 7911 123456 | Two-factor code never arrives by SMS. Tried resending four times over twenty minutes. |
That output is useful precisely because it avoids both failure modes: it doesn't look like obviously fake filler ("Test User", "555-0100" repeated on every row), and it isn't traceable to anything real because nothing real was ever in the prompt. The country-code mix also forces your phone validation to handle formats beyond your default locale, which is its own quiet source of bugs.
Prompting principles that keep the data usable
All three templates above share the same underlying habits. These are the prompt engineering fundamentals applied specifically to data generation:
State the schema, not just the topic. Field names, types, and valid ranges, the same information you'd put in a CREATE TABLE statement or a validation schema.
Name the output format explicitly. "CSV with a header row," "a JSON array of objects with these exact keys," or "SQL INSERT statements for the orders table." If you need parseable JSON specifically, the rules for getting clean JSON output from AI apply here too.
Ask for a small sample before scaling up. Five to ten rows is enough to catch a broken constraint. Fixing the prompt after row 8 is far cheaper than fixing it after row 4,000.
Push high volume into a script, not a chat completion. Faker, SQL generators, or a short program the model writes for you, seeded with the rules you already validated in the sample.
Spell out relationships between tables explicitly. "user_id must match an existing users.id" is a sentence a schema diagram implies but a prompt has to say out loud.
Frequently asked questions
Can AI generate test data that matches my exact database schema?
Yes, and it works better than describing the schema in prose. Paste your actual CREATE TABLE statements, or a compact type list, directly into the prompt. The closer your input is to the real schema, the closer the output matches it, including types you might forget to mention verbally, like a NUMERIC(10,2) column that shouldn't get a 3-decimal value.
Is AI-generated test data safe to use instead of real customer data in staging?
For structural and volume testing, yes, that's the whole point. For catching bugs that only show up with genuinely messy production patterns, synthetic data has limits; it reflects the constraints you described, not the actual chaos of real usage. Never feed real customer records into the prompt as a seed or example, even to "make it more realistic."
How do I get AI to generate thousands of rows of test data?
Don't ask for the rows themselves. Ask for a script, a Python snippet using Faker with the constraints you specified, or a SQL statement built on generate_series, and run that locally. Direct generation in a single response starts repeating values and drifting from constraints well before you reach four figures of rows.
Will AI-generated test data include duplicates?
At small volumes, rarely, if your constraints are specific. Past a couple hundred rows requested in one completion, yes, names and patterns start repeating because the model is sampling from a narrower effective space than it seems. That's the practical ceiling for asking for literal rows instead of a generator script.
What format should I ask for when generating test data with AI?
Match it to how you'll load it: JSON for mocking an API response, CSV for a bulk import tool, SQL INSERT statements for seeding a database directly, or a markdown table when you just need to eyeball it before deciding. Naming the format is one more constraint that keeps the model from improvising.
How did this land?
About the author

Developer Advocate
Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.


