Obaid Sajjad

· Obaid Sajjad

Generating Test Data with LLMs: Fixtures Without the PII Risk

  • test-data
  • llm-testing
  • privacy
  • test-automation

The QA environment had 40,000 user records in it, copied from production six months ago. The engineering team had been using it for testing for years. It was reliable, it was realistic, and it had names, email addresses, and in some cases partial payment information for real customers who had no idea they were in a test database that a dozen engineers could access.

This is a common situation and an increasingly regulated one. GDPR, CCPA, and similar frameworks treat production data in test environments as a compliance exposure. A breach of test environment data is a breach of customer data, regardless of the intent behind copying it there. The “we only use it for testing” defense does not hold up in a data protection audit.

LLMs offer a different approach: generate test data that is realistic enough to stress your application the way production data would, without containing any real customer information. The generated data can be domain-specific, structurally valid, edge-case-rich, and arbitrarily large, produced on demand rather than copied from production. This post covers how to build a test data generation system that delivers on those properties without reintroducing the problems it was meant to solve.

Why Synthetic Data Generation Is Now Practical

Synthetic data generation is not new. Tools for generating fake names, addresses, and structured records have existed for decades. The challenge has always been that generic fake data generators produce data that looks synthetic: the names are too common, the addresses do not match realistic distributions, the free-text fields are obviously templated, and the edge cases that stress a real application are missing.

LLMs change the equation in two ways. First, they can generate text that reads like real user input rather than template output. A free-text support ticket generated by an LLM can have the same ambiguity, misspelling, emotional register, and implicit context that real support tickets have, because the model learned from text that humans actually wrote. Second, LLMs can be instructed to generate specific types of edge cases: the angry customer, the request that is ambiguous between two categories, the user who provides contradictory information across fields, the query written in a language other than the expected one.

These capabilities let you design a test data corpus rather than sample one. Instead of hoping production data contains the edge cases you need, you specify the edge cases you need and generate examples of each. The test data becomes a deliberate artifact of your QA process rather than an accident of what users happened to do before you copied the database.

The Generation Architecture

A practical LLM-based test data generator has three layers: a schema definition layer, a generation layer, and a validation layer.

The schema definition layer describes the structure of the data your application handles and the constraints each field must satisfy. For a user record, this might specify that the email field must be a valid email address, the account creation date must be between 2018 and 2025, the subscription tier must be one of three defined values, and the usage statistics must be numerically consistent with the subscription tier. Schema definitions can be written as JSON Schema, as Zod schemas if you are already using TypeScript validation, or as structured text that you include in the generation prompt.

The generation layer sends the schema definition plus a persona or scenario description to the LLM and asks it to produce records that match the schema and fit the persona. A prompt that asks for “a record for a user who has been on the premium tier for two years, uses the product daily, and recently downgraded to the free tier” will produce a record that is internally consistent with that narrative, which is more useful for testing the downgrade flow than a randomly generated record.

The validation layer checks every generated record against the schema before adding it to the test dataset. LLMs are not perfect at following structural constraints, and occasionally generate records where a field value violates a schema rule or where fields that should be consistent are not. Validating against the schema catches these at generation time, before they cause confusing test failures. Records that fail validation are regenerated rather than used.

Designing Persona-Based Generation

The most useful test datasets represent the range of user personas and behaviors your application actually handles, plus the edge cases that are underrepresented in production data but important to test.

Start by identifying the major user segments for the feature you are testing. For a billing system, these might be: a long-time customer with a stable subscription, a customer who recently joined and has not yet made their first payment, a customer who has had billing failures and a payment method on file that needs updating, a customer who has multiple accounts under the same email, and a customer who has requested account deletion but the process is not complete. Each of these is a persona that represents a distinct state the billing system must handle correctly.

Write a persona description for each segment. The description does not need to be long: two to three sentences describing the user’s history, their current state, and anything unusual about their account. Give this description plus the schema definition to the generation layer and ask for 20 records matching the persona. You now have 20 examples of the billing-failure scenario, which is probably more than production sampling would give you for a scenario that occurs 2% of the time.

Add edge-case personas deliberately. For most applications, there are scenarios that almost never occur in production but cause significant issues when they do: the user whose name contains Unicode characters that the display layer does not handle, the user whose address is at a valid address that nonetheless fails geocoding, the user who has been a customer for longer than the application was designed to support. Generate a small set of these edge cases explicitly rather than hoping they appear in your sampled production data.

Free-Text Fields and the Realism Problem

Structured fields (IDs, dates, enum values, numbers) are straightforward to generate and validate. Free-text fields are harder, because realism for free text means something different from structural validity.

A realistic support ticket is not just text that describes a problem. It is text that reflects how a real user writes when they are frustrated, or confused, or in a hurry. It has the irregular capitalization, the missing punctuation, the “btw” and “lol” that template generators cannot produce. It references context that the writer assumes the reader shares. It asks multiple questions in a single message. It contains information in an order that does not match your intake form’s fields.

LLMs generate this kind of text naturally if you prompt for it correctly. The key is to prompt for the emotional and contextual state of the writer, not just the content: “Write a support ticket from a user who has been trying to reset their password for 45 minutes, has tried three times, and is now worried that their account has been locked. The user is not technical and is writing on their phone.”

For testing NLP features, named entity recognition, classification, sentiment analysis, or intent detection, realistic free text is essential. Generated free text that reads like templates will make these features appear to perform better than they actually do, because the templated text is easier to classify than real user text. Persona-driven generation with emotional and contextual specifications produces text that is closer to the actual distribution your NLP features will encounter.

Keeping Generated Data PII-Free

The point of generating test data is to avoid PII, but there are two ways generated data can inadvertently reintroduce it.

The first is memorized data. LLMs have memorized some real personal information from their training data: names of public figures, addresses of public buildings, valid-format email addresses that belong to real people. Generating a record for “a customer named John Smith in New York” might produce a real email address for a real person by coincidence. Filtering generated data for recognizable PII patterns (real-format email addresses from real domains, phone numbers in real area codes with real exchange prefixes) reduces this risk.

The second is structure-based reconstruction. If your schema includes enough correlated fields (zip code, income range, age range, industry), the combination might be specific enough to identify a real individual even though no single field is PII. This is the de-identification problem that data science teams work on. For most testing scenarios, the combination of fields required for re-identification is unlikely to occur in generated test data, but for highly sensitive domains (healthcare, financial services), run generated records through a de-identification assessment before using them at scale.

Use obviously synthetic identifiers where possible. Email addresses in the format testuser-<uuid>@test.example are structurally valid (which is necessary for email validation tests) but clearly synthetic. Names from a named character set that does not match real-world name distributions (if your personas can use clearly fictional names without breaking your test assertions) eliminate one category of risk entirely.

Integration With Your Test Suite

Generated test data is most useful when it is integrated into the test suite as a first-class artifact rather than a one-time export.

Static fixtures: generate a fixed set of records for each persona, check them into source control alongside your test code, and use them as the fixtures for your deterministic test cases. These fixtures are reviewed and approved the same way test code is reviewed. When you add a new persona, you add a new fixture file. When the schema changes, you regenerate the affected fixtures and review the changes in the PR.

Dynamic generation: for property-based testing or fuzz-like test approaches, generate fresh data on each test run using the schema and persona definitions as the source of truth. This catches failures that static fixtures would not surface, because each run exercises slightly different values. The trade-off is reproducibility: a failure triggered by dynamically generated data is harder to reproduce unless you log the seed or the specific generated values that triggered it.

Seeding test environments: use the generation layer to populate test environment databases with realistic-scale data. If your application behaves differently with 10 records than with 100,000 records (pagination, query performance, indexing), generate the larger dataset to match production scale. This is the scenario where LLM generation is most valuable compared to template generators: generating 100,000 realistic support tickets with varied content that stress-tests your search and classification features is not practical with templates but is straightforward with an LLM generation pipeline running overnight.

The Compliance Argument for Synthetic Data

The engineering argument for LLM-generated test data is capability: better coverage, better edge cases, better realism. The business argument is compliance.

Most data protection regulations treat personal data in test environments as a liability. The controls required to secure test data that contains PII are largely the same as the controls required to secure production data: access controls, encryption at rest, audit logging of access, data retention policies, breach notification procedures. These controls cost time and money to implement and maintain. They are also frequently incomplete in test environments, which organizations notice during audits.

Synthetic test data that contains no PII by construction eliminates this category of liability. Test environments with synthetic data can be shared more broadly, accessed without the restrictions that PII access requires, and deprecated without the data deletion procedures that regulated data requires. The engineering team can spin up a new test environment from scratch, seed it with generated data, and share credentials freely without any of the process overhead that production-data test environments require.

This is the argument that typically gets traction with security and compliance stakeholders. The capability improvements are real and matter to engineers. The compliance simplification is what matters to the people who approve engineering investment, and it makes the case for building the generation infrastructure in terms that do not require any understanding of test data quality to evaluate.