Test Data Generation Guide: Build Fixtures That Check Real Rules
Build test data from schema constraints, parent-child relationships and expected outcomes. Includes a worked order fixture and field-specific examples.
By Vincent RuanPublished June 25, 2026Last updated September 6, 20265 min read

A test data generation guide should leave you with inputs and expected outcomes. Start by naming the behavior: a checkout accepts an existing customer, an import preserves a leading-zero postcode, or an API rejects an impossible date. Generate data to exercise that rule, then verify the result.
This workflow covers application fixtures and database seeding. For ready-made person rows, the bulk exporter provides CSV, JSON and SQL. Custom entities, foreign keys and business constraints still belong in your fixture builder.
1. Write a small data contract
| Field | Success-path requirement | Intentional failure case |
|---|---|---|
| customer_id | Matches an existing customer key | Unknown customer c999 |
| amount_cents | Integer greater than zero | Zero or negative amount |
| ordered_at | ISO timestamp in UTC | Unparseable timestamp |
| shipped_at | Null or at/after ordered_at | Shipment before order |
| A fictional example-domain value | Malformed input in the validation suite | |
| postal_code | Text, preserving leading zeros | Numeric import removes a leading zero |
A positive amount is the rule for this import example; a refund system could need negatives. Make that distinction explicit before generating. Otherwise a generic validator can reject legitimate domain data while the test suite appears correct.
2. Generate parents before children
Create customers c001, c002 and c003 first. Then pick order.customer_id values from those three keys. Do not generate independent random customer IDs for the orders and hope they join. The deliberately invalid order below belongs in a separate rejection test.
| Order | customer_id | amount_cents | Expected result |
|---|---|---|---|
| o001 | c001 | 1299 | Accept |
| o002 | c001 | 500 | Accept; one customer may have multiple orders |
| o003 | c003 | 2500 | Accept |
| o004 | c999 | 1000 | Reject unknown customer |
| o005 | c002 | -100 | Reject amount under this contract |
This five-row example contains three accepted rows and two rejected rows by construction. Assert each result and its error reason. Import only the first three when setting up a successful order-list test; use the other two to test the importer’s rejection behavior. The counts are fixture arithmetic, not a production error-rate measurement.
3. Choose a generation method by the constraint
| Method | Use when | Work you still own |
|---|---|---|
| Hand-written fixtures | A small set of named boundary or regression cases matters | Maintain expected outcomes when the contract changes |
| Faker or a similar library | You need many ordinary rows in an automated suite | Domain relationships, unique keys and explicit edge cases |
| A browser exporter | Someone needs a file without writing generation code | Map its fixed fields into the target schema |
| A controlled source-derived dataset | A test needs measured distributions or a specific production defect | Access, provenance, disclosure review and refresh policy |
Do not assume that a masked production copy is anonymous, or that every synthetic generator is independent of source records. NIST SP 800-188 describes de-identification approaches and the governance needed around them [nist]. If your test needs no production records, a source-independent fixture avoids copying those records in the first place.
4. Apply the right rule to each field
| Field | Useful fixture choice | Limit |
|---|---|---|
| reader@example.com for a display or syntax case [iana-domains] | Use a controlled mail sink to test actual messages | |
| Card payment | A documented processor test scenario [stripe] | A Luhn pass is not authorization; use test credentials |
| SSN-shaped input | An excluded area such as 000 in a rejection test [ssa] | Nine-digit shape does not make an issued or valid SSN |
| Postal address | A verified city/region/postcode tuple plus a fictional street | A matching tuple does not verify delivery to that street |
| Birthday | A fixed reference date and named leap-day policy | A random date need not cover age boundaries |
Use the US address format guide for field layout and the Stripe test-card guide for processor scenarios. A country code chooses conventions; it does not independently validate geography or a delivery point.
5. Add explicit boundaries beside ordinary rows
Do not wait for random generation to produce the case you need. The four validation articles provide fixed downloadable inputs with stated expectations: names, emails, phone numbers and birthdays.
- For a text field with a declared maximum, test one below, exactly at and one above it; state whether the limit counts bytes, code points or another unit.
- For an optional field, test missing, null and empty string separately when your schema distinguishes them.
- For a timestamp pair, test equal times, correct order and reversed order.
- For a relationship, test no children, one child, several children and an orphan key.
6. Record the recipe and verify the import
Faker documents why a seed is insufficient when dependencies change or a method uses a relative date [faker]. Record the exact version, locale, seed, call order and reference date. The deterministic seeding guide includes a runnable example with measured output.
- Validate the success-path file against the real schema before importing.
- Run rejection fixtures separately and assert the named errors.
- Check row counts, key uniqueness, joins and date ordering after import.
- Import postal codes and other identifiers as text; confirm that leading zeros and Unicode survive.
- Reset or namespace generated records so rerunning a test does not introduce duplicates.
- Save the exact failing row with its configuration when debugging a regression.
A generated dataset is useful when you can say what it tests and show the expected result. Large row counts help load tests; a five-row fixture may be enough to prove a relationship rule. Choose the smallest dataset that answers the test’s question, then add volume only when volume is part of that question.
References & sources
- SP 800-188: De-identification techniques and governance — NIST
- Example Domains — IANA
- Testing payments with test cards and credentials — Stripe
- Social Security Number Randomization: excluded numbers — Social Security Administration
- Faker usage: reproducible results — Faker
Frequently asked questions
How do I generate useful test data?
Define the test behavior and field constraints, create parent records, generate children from existing keys, add deliberate boundary cases, and check expected outcomes before importing. Record the generation configuration and reset the environment between runs.
Should all generated rows pass validation?
Only the success-path dataset should. Rejection tests need deliberately invalid rows with named expected errors. Keep them separate so an intentional error cannot accidentally break setup for an unrelated test.
Does a country locale make an address valid?
No. A locale chooses language and formatting conventions. City, region and postcode need a verified relationship, and a full street address needs separate delivery-point validation when the test requires actual delivery.
Is a random Luhn-valid number a payment test card?
No. The checksum only detects certain input errors. Use your processor’s documented test card or payment method with test credentials, and choose the documented scenario you want to exercise.
Does a seed reproduce an entire dataset?
Only with the same version, source data, locale, generation order and clock inputs. Record those settings and preserve important failing rows as fixed fixtures.