Quality is not optional. It's our standard. Free QA tools for testers and developers.

How to Generate Test Data for Software Testing: A Practical Workflow

How to Generate Test Data for Software Testing: A Practical Workflow

Test data is the fuel that powers every test case, yet it is often the most neglected part of a QA pipeline. Teams spend weeks perfecting automation frameworks, only to discover that flaky tests are caused by stale, incomplete, or unrealistic data. This post walks through a repeatable, evidence‑driven workflow for creating test data that matches the complexity of real‑world systems while staying maintainable and compliant.


1. Understand the Problem Space

SymptomRoot CauseImpact
Tests pass locally but fail in CIData schema drift between environmentsFalse confidence, wasted debugging time
Data‑privacy tickets block releasesProduction copies used without maskingLegal risk, delayed deployments
Test suites run for hoursMassive, unpartitioned data setsSlow feedback loops, higher cloud costs
Edge‑case bugs slip to productionOnly “happy‑path” records generatedMissed regression coverage

If any of these look familiar, the workflow below will help you move from ad‑hoc scripts to a governed data‑generation process.


2. Prerequisites – What You Need Before Writing a Single Generator

PrerequisiteWhy It MattersHow to Verify
Domain model documentation (ER diagrams, API contracts)Guarantees referential integrity across tables/servicesReview with a developer or architect
Data‑classification policy (PII, PCI, PHI)Drives masking / synthetic‑data decisionsCheck compliance checklist
Target environments (dev, staging, performance, chaos)Determines volume, refresh cadence, and isolation needsList each environment and its SLA
Tooling inventory (DB clients, CI/CD, container runtime)Influences implementation choice (SQL scripts, code‑based generators, SaaS)Document versions and access rights
Version‑control strategy (monorepo vs. per‑service)Affects how generator code is packaged and promotedAgree on branching model

Tip: Capture these items in a lightweight “Data‑Generation Charter” (one‑page markdown) and store it next to your test‑automation repo. It becomes the single source of truth for newcomers.


3. Decision Criteria – Choosing a Generation Strategy

StrategyWhen It ShinesTrade‑offsTypical Tooling
Production clone + maskingExisting large datasets, need realistic distributionRequires robust masking pipeline; risk of residual PIICustom ETL, Delphix, Tonic
Pure synthetic generationGreenfield projects, strict privacy, need deterministic seedsMay miss hidden correlations; requires domain knowledgeQA3 free test data generator (/tools/test-data-generator), Faker libraries, Hypothesis
Hybrid (seed + synthetic)Legacy systems with partial data, want realistic volume + privacyMore complex orchestrationCombination of above
Snapshot‑based (DB snapshots / container volumes)Performance / load testing where exact data shape mattersSnapshots age quickly; storage costDocker volumes, Cloud provider snapshots
Contract‑driven (OpenAPI / GraphQL schema)API‑first teams, need request/response payloadsLimited to schema‑defined fieldsSchemathesis, Prism, custom codegen

Decision flowchart (textual):

  1. Do you have a production dataset you can legally copy? → Yes → Mask & subset → Production clone
  2. Is the schema stable and fully documented? → Yes → Pure synthetic (seedable)
  3. Do you need both realistic volume and strict privacy? → Yes → Hybrid (seed core tables, synthesize peripheral)
  4. Is the test a one‑off load run? → Yes → Snapshot

Pick the strategy per test suite; a single project often mixes several.


4. The End‑to‑End Workflow

Below is a repeatable, CI‑friendly pipeline. Each step produces an artifact that can be versioned, reviewed, and audited.

4.1 Requirements Gathering (Story‑Level)

  1. Identify the test scope – unit, integration, contract, performance, security.
  2. List required entities – e.g., User, Order, Payment, Inventory.
  3. Define constraints – uniqueness, foreign‑key ranges, enum values, date windows.
  4. Capture data‑quality rules – “email must be unique”, “order.total = sum(lineItems)”.

Artifact: test-data-requirements.md (markdown table, one row per entity).

4.2 Data Profiling (If Cloning)

ActionToolOutput
Schema extractionpg_dump --schema-only, mysqldump -dschema.sql
Statistics collectionANALYZE, DBMS_STATSstats.json (row counts, distinct values, histograms)
Referential‑integrity checkCustom script / pg_constraintfk-report.csv
PII scanpii-scanner (open‑source)pii-findings.csv

Store all outputs in profiling/ folder, commit to repo.

4.3 Strategy Selection & Design

  • Create a decision matrix (see Table in Section 3) for each entity.
  • Define seed values for deterministic runs (e.g., SEED=20240315).
  • Draft generator contracts – JSON/YAML that describe each generator’s input/output. Example:
generators:
  - name: user
    strategy: synthetic
    count: 1000
    fields:
      id: uuid
      email: "{{unique}}@example.com"
      role: [admin, user, guest]
      created_at: "{{date_between '2020-01-01' '2023-12-31'}}"
  - name: order
    strategy: hybrid
    seed_table: user
    count_per_user: 3
    fields:
      id: uuid
      user_id: "{{ref user.id}}"
      status: [new, paid, shipped, cancelled]
      total: "{{calc_sum line_items.amount}}"

Artifact: generation-plan.yaml – reviewed in PR.

4.4 Implementation

Language / FrameworkTypical Use‑CaseExample Snippet
Python + FakerQuick scripts, CI jobsfake = Faker(); fake.unique.email()
Java + DataFactorySpring‑Boot integration testsUserFactory.createBatch(500)
SQL (CTE + generate_series)Bulk DB seeding, no app codeINSERT INTO users SELECT ... FROM generate_series(1,1000)
QA3 Test Data GeneratorZero‑code, UI‑driven, exportable CSV/JSON/SQLUse the web UI at /tools/test-data-generator to model the plan, then download a ready‑to‑run SQL file.
Terraform / HelmProvisioning test DBs with data in Kuberneteshelm install test-db ./charts/postgres --set initScript=seed.sql

Best practice: Keep generator code pure (no external side‑effects) and parameterised (counts, date ranges, seed). This makes the same generator reusable across environments.

4.5 Validation – The “Data‑Quality Gate”

Run a validation suite before the data is promoted to any test environment.

CheckImplementationPass Criteria
Schema conformanceSELECT * FROM information_schema.columns …All required columns present, correct types
Referential integritySELECT COUNT(*) FROM orders LEFT JOIN users … WHERE users.id IS NULLZero orphan rows
Business rule complianceCustom SQL / Python asserts (assert order.total == sum(line_items.amount))100 % pass
Statistical similarity (if cloning)KS‑test on numeric columns, chi‑square on categoricalp‑value > 0.05
Privacy scanRun same PII scanner as profilingZero findings
DeterminismRun generator twice with same seed, diff outputIdentical byte‑for‑byte (or deterministic ordering)

Automate these checks in a GitHub Actions / GitLab CI job named validate-test-data. Fail the pipeline on any violation.

4.6 Provisioning & Versioning

  1. Package the validated output (SQL dump, CSV set, JSON fixtures) as an artifact (e.g., test-data-v1.4.0.tar.gz).
  2. Tag the artifact with semantic version matching the generator code (v1.4.0).
  3. Publish to an internal artifact store (Nexus, Artifactory, S3 with versioning).
  4. Consume in downstream pipelines via a lightweight “data‑loader” step:
- name: Load test data
  run: |
    curl -L $ARTIFACT_URL -o data.tar.gz
    tar -xzf data.tar.gz
    psql -h $DB_HOST -U $DB_USER -d $DB_NAME -f seed.sql

4.7 Maintenance & Evolution

TriggerAction
Schema migration (new column, FK)Update generation-plan.yaml, bump minor version, re‑run validation
New privacy regulationAdd masking rule, regenerate synthetic data, retire old artifacts
Performance test scale‑upIncrease count parameters, verify load‑test SLAs
Flaky test traced to dataAdd a targeted negative‑case generator (e.g., malformed email) and extend validation

Schedule a quarterly data‑health review (30 min) with a QA lead, a developer, and a data‑privacy officer.


5. Worked Example – E‑Commerce Order Service

Below is a concrete walk‑through using the hybrid strategy (synthetic users, seeded product catalog, synthetic orders). The full generator lives in a single Python script (generate.py) that can be executed locally or in CI.

5.1 Requirements (test-data-requirements.md)

EntityVolumeKey Constraints
user5 000Unique email, role ∈ {admin, customer}, created_at ≤ today
product1 200 (seed)SKU unique, price > 0, active boolean
order15 000FK → user, status ∈ {new, paid, shipped, cancelled}, total = Σ line items
order_line45 000FK → order, FK → product, qty ≥ 1, line_total = qty × product.price

5.2 Profiling (seed product catalog)

pg_dump --data-only -t product prod_db > seed_product.sql


# Run PII scanner – none found


5.3 Generation Plan (generation-plan.yaml)

generators:
  - name: user
    strategy: synthetic
    count: 5000
    seed: 20240315
    fields:
      id: uuid
      email: "{{unique}}@shop.example.com"
      role: [admin, customer]
      created_at: "{{date_between '2020-01-01' '2024-03-01'}}"


- name: product
    strategy: seed
    source: seed_product.sql
    # No transformation needed


- name: order
    strategy: synthetic
    count: 15000
    seed: 20240315
    depends_on: [user, product]
    fields:
      id: uuid
      user_id: "{{ref user.id}}"
      status: [new, paid, shipped, cancelled]
      created_at: "{{date_between '2023-01-01' '2024-03-01'}}"
      # total calculated later


- name: order_line
    strategy: synthetic
    count_per_order: 3
    depends_on: [order, product]
    fields:
      id: uuid
      order_id: "{{ref order.id}}"
      product_id: "{{ref product.id}}"
      quantity: "{{random_int 1 5}}"
      line_total: "{{calc quantity * ref product.price}}"

5.4 Implementation (excerpt)



# generate.py


import yaml, uuid, random, datetime
from faker import Faker


fake = Faker()
fake.seed_instance(20240315)
random.seed(20240315)


def load_plan(path):
    with open(path) as f:
        return yaml.safe_load(f)


def gen_users(n):
    for _ in range(n):
        yield {
            "id": str(uuid.uuid4()),
            "email": fake.unique.email(domain="shop.example.com"),
            "role": random.choice(["admin", "customer"]),
            "created_at": fake.date_between(start_date="-4y", end_date="today")
        }


def gen_orders(users, products, n):
    for _ in range(n):
        user = random.choice(users)
        status = random.choice(["new", "paid", "shipped", "cancelled"])
        created = fake.date_between(start_date="-1y", end_date="today")
        yield {
            "id": str(uuid.uuid4()),
            "user_id": user["id"],
            "status": status,
            "created_at": created,
            "lines": []   # filled later
        }


def gen_lines(orders, products, per_order):
    for order in orders:
        for _ in range(per_order):
            prod = random.choice(products)
            qty = random.randint(1, 5)
            line_total = round(qty * prod["price"], 2)
            order["lines"].append({
                "id": str(uuid.uuid4()),
                "order_id": order["id"],
                "product_id": prod["id"],
                "quantity": qty,
                "line_total": line_total
            })
        order["total"] = round(sum(l["line_total"] for l in order["lines"]), 2)


# -----------------------------------------------------------------


plan = load_plan("generation-plan.yaml")
users = list(gen_users(plan["generators"][0]["count"]))
products = load_seed_products("seed_product.sql")   # helper omitted
orders = list(gen_orders(users, products, plan["generators"][2]["count"]))
gen_lines(orders, products, plan["generators"][3]["count_per_order"])


# Write CSVs for each table (or SQL INSERTs)


write_csv("users.csv", users, ["id","email","role","created_at"])
write_csv("products.csv", products, ["id","sku","price","active"])
write_csv("orders.csv", [{k:v for k,v in o.items() if k!="lines"} for o in orders],
          ["id","user_id","status","created_at","total"])
write_csv("order_lines.csv", [l for o in orders for l in o["lines"]],
          ["id","order_id","product_id","quantity","line_total"])

Running python generate.py produces four CSV files that pass the validation suite (see Section 4.5). The same script is executed in CI; the artifact test-data-v1.2.0.tar.gz is uploaded to Artifactory.

5.5 Validation Results (sample CI log)

✔ Schema conformance – all 4 tables match target DDL
✔ Referential integrity – 0 orphan order_lines, 0 orphan orders
✔ Business rule – order.total == Σ line_total (15

Read more

Test Data Generation Strategy: From Requirements to Reusable Datasets

A ready-to-use companion to “Test Data Generation Strategy: From Requirements to Reusable Datasets,” including decision points, examples, ownership guidance, and review criteria.

Test Data Generator Checklist for QA Teams

A ready-to-use companion to “Test Data Generator Checklist for QA Teams,” including decision points, examples, ownership guidance, and review criteria.

How to Create Test Data Without Copying Production Records

A step-by-step guide for “How to Create Test Data Without Copying Production Records,” covering prerequisites, implementation choices, validation, and common failure modes.