Skip to content
0degrees.ai
Prompt Engineering

Generating Realistic Test Fixtures with AI

How to prompt AI coding assistants to produce diverse, realistic test fixtures and seed data — not just placeholder values that pass but don't catch real bugs.

0degrees Team 6 min read

When you ask an AI to “add some test data,” you get user1, [email protected], and password123. The tests pass. Then production gives you a user whose display name is 魏小龙, an email address with a plus sign, and a date-of-birth in 1899 — and the bug surfaces in front of an actual person.

The failure isn’t the model’s fault. “Add some test data” is underspecified. A model generating plausible training-data-shaped output will produce the most average possible fixture. Average fixtures miss the edges, and the edges are where real bugs live.

The fix is in the prompt. AI is genuinely good at fixture generation — it knows your domain, it can reason about edge cases, and it can produce large diverse datasets on demand. But you have to ask for those things explicitly.

Why AI defaults to trivial fixtures

Models generate what’s statistically most likely given the prompt. “Create a User object for testing” produces the median user: an ASCII name, a simple email, a round-number age. That’s a valid data point, but it’s the least interesting one.

The problem compounds when the same trivial fixture gets reused across dozens of tests. You’ve effectively tested one data point repeatedly, with high confidence that the one data point works. The things that break in production — Unicode, null optionals, values at boundaries, fields that interact in unexpected ways — were never exercised.

The solution isn’t to write fixtures by hand. It’s to give the AI the context it needs to produce non-trivial ones.

Pattern 1: State the domain before asking for data

Fixtures that don’t reflect the actual domain don’t protect you. Before asking for test data, give the model enough context to generate plausible examples:

Context: this is an e-commerce system. Orders can be placed by guest users
(no account) or authenticated users. An order has 1–50 line items. Line items
reference products by SKU. Some SKUs are discontinued and have a null price.
Shipping address is required; billing address is optional and defaults to
shipping if absent.

Generate 5 Order fixtures that cover meaningfully different scenarios.

The model now knows what “realistic” means in this domain. It will produce a guest order, an order at the line-item limit, an order with a discontinued SKU, and orders with and without an explicit billing address — because you named those dimensions explicitly.

Without the domain context, it produces five essentially identical orders that differ only in ID.

Pattern 2: Ask for diversity by constraint, not by count

“Give me 10 fixtures” produces 10 copies of the same fixture with different IDs. The better ask names the axes of variation you want covered:

Generate User fixtures that cover these cases:
- Display name: ASCII only, Unicode (CJK), emoji, very long (>50 chars), single character
- Email: standard format, plus-addressed ([email protected]), subdomain
- Account status: active, suspended, pending verification
- Locale: en-US, ja-JP, ar-SA (right-to-left)

One fixture per case. Use realistic-looking values, not "test1", "test2".

You’re not asking for more data — you’re asking for data that spans the shape of the input space. This approach also documents your fixture set: the list of cases is a checklist of what you’ve actually tested.

// What this prompt produces — varied and actually useful
export const users = {
  asciiName: { displayName: 'Sarah Chen', email: '[email protected]', status: 'active', locale: 'en-US' },
  cjkName: { displayName: '魏小龙', email: '[email protected]', status: 'active', locale: 'ja-JP' },
  plusEmail: { displayName: 'Dev Tester', email: '[email protected]', status: 'pending', locale: 'en-US' },
  suspended: { displayName: 'Banned User', email: '[email protected]', status: 'suspended', locale: 'en-US' },
  rtlLocale: { displayName: 'أحمد محمود', email: '[email protected]', status: 'active', locale: 'ar-SA' },
} satisfies Record<string, User>;

Pattern 3: Ask for boundary values explicitly

Boundary conditions require explicit prompting. Models don’t generate them spontaneously because they aren’t statistically common in training data — but they’re disproportionately common in bug reports.

Generate numeric fixtures for a payment system that processes amounts in cents.
Include:
- The minimum valid charge ($0.50, i.e. 50 cents)
- One cent below minimum (49 cents — should be rejected)
- A typical amount ($29.99)
- A large but valid amount ($9,999.99)
- The maximum the system supports ($99,999.99)
- One cent over maximum (should be rejected)
- Zero (should be rejected)
- Negative value (should be rejected)

The model generates these correctly because you named the boundaries. Without naming them, you get $10.00, $25.00, $100.00 — all valid, all uninteresting, all far from the edges that actually matter.

Pattern 4: Drive from your schema

The most reliable fixture generation uses your actual types as the specification. Paste your schema into the prompt and ask the model to produce fixtures that satisfy it while covering the optional fields and nullable columns:

Given this TypeScript type, generate 4 fixtures. The first should use only
required fields. The second should use all optional fields. The third should
have nulls in every nullable field. The fourth should reflect a real-world
edge case you'd expect in production data.

type Subscription = {
  id: string;
  userId: string;
  plan: 'free' | 'pro' | 'enterprise';
  status: 'active' | 'canceled' | 'past_due' | 'trialing';
  trialEndsAt: Date | null;
  canceledAt: Date | null;
  seats: number;
  metadata: Record<string, string>;
};

The schema-driven approach ensures every fixture is structurally valid before you run a single test. The model knows the constraints — it just needs to be told to exercise them.

Pattern 5: Ask the model what you missed

After generating an initial fixture set, add one more prompt:

Looking at these fixtures, what edge cases in the domain are NOT covered?
List them concisely — don't generate the data yet.

The model is often good at naming the cases it didn’t produce spontaneously. You’ll see things like “a user with an unverified email who has placed an order,” “a subscription trial that has already expired,” or “an order with a zero-quantity line item.” Decide which ones are worth adding, then ask for exactly those fixtures.

This step is especially valuable when you’ve been close to a domain for a long time — your mental model of “normal” data shapes your fixture defaults in the same way it shapes the model’s. An explicit gap-finding pass surfaces the cases both of you were implicitly ignoring.

Keeping fixtures maintainable

One risk of AI-generated fixtures is quantity without structure. A file with 40 fixtures in a flat array is hard to navigate and harder to extend. Ask for named exports grouped by the dimension they test:

Structure the fixtures as named TypeScript exports, grouped into a const object
by the scenario they represent. Use descriptive names, not indexes.
Export a flat array called `all` for tests that iterate over all of them.

The resulting fixture file reads like documentation:

export const subscriptions = {
  activeProWithTrial: { ... },
  canceledMidCycle: { ... },
  pastDueEnterprise: { ... },
  freeWithExpiredTrial: { ... },
};

export const all = Object.values(subscriptions);

Each test can reach for exactly the fixture it needs by name, and new cases can be added without touching existing ones.

The prompting principle

The pattern across all of these techniques is the same as elsewhere in AI-assisted development: the model can only cover the cases you describe. “Some test data” produces some test data — unspecified quantity, unspecified distribution, unspecified edge cases. Naming the dimensions of variation, the boundary conditions, and the domain constraints transforms fixture generation from a time-filler into a genuine testing asset.

This pairs directly with the test generation workflow in Writing Tests with AI — once you have well-structured, diverse fixtures, the model can write meaningful parameterized tests against them, and the combination gives you actual coverage rather than the illusion of it. And if you’re specifying types and interfaces before asking for data, Type-Driven AI Development covers how to use your type system as the authoritative spec that keeps fixtures and implementation in sync.

Fixtures are part of your codebase. They deserve the same intentional specification as the code they test.

[ Related ]

Keep reading