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

Oracle Test Data Generation: A Practical QA Guide

Oracle Test Data Generation: A Practical QA Guide

Generating realistic, repeatable test data for Oracle databases is one of the most time‑consuming parts of a QA cycle. Teams that treat it as an after‑thought end up with flaky tests, hidden bugs, and costly production incidents. This guide walks through the decisions, tools, and validation steps that turn test‑data generation from a chore into a reliable, repeatable process.


1. Why Test Data Is a First‑Class Concern

SymptomRoot causeImpact
Tests pass locally but fail in CIData‑state drift between environmentsLost confidence, wasted debugging time
“Works on my machine” bugs surface only in stagingSynthetic data does not exercise real constraints (FK, CHECK, triggers)Defects escape to production
Performance tests give misleading numbersData volume or distribution does not match productionCapacity planning based on false premises
Data‑privacy violations in non‑prodProduction clones used without maskingLegal risk, audit findings

If any of these sound familiar, the problem is usually how the data is created, not whether you have a test environment.


2. Oracle‑Specific Challenges

FeatureWhy it matters for test data
PL/SQL packages & triggersBusiness logic lives in the database; data must satisfy pre‑/post‑conditions.
Partitioned tables & sub‑partitionsPartition pruning changes query plans; test data must reflect realistic partition boundaries.
Advanced security (VPD, OLS, Real Application Security)Row‑level policies filter data at runtime; generators must respect policy predicates.
Materialized views & refresh groupsStale MV data can hide bugs; refresh schedules need representative source data.
LOBs, XMLType, JSONLarge objects require streaming inserts; simple INSERT … VALUES often fails.
Flashback & undo retentionTests that rely on flashback queries need a consistent undo window.

A generic CSV‑loader will not exercise any of the above. The generator must understand Oracle’s metadata (constraints, defaults, virtual columns, identity columns) and be able to produce data that survives the full DML path.


3. Decision Criteria for a Test‑Data Generation Approach

CriterionQuestions to askWeight (1‑5)
Schema awarenessDoes the tool read the data dictionary (ALL_TAB_COLUMNS, ALL_CONSTRAINTS, etc.)?5
Referential integrity handlingCan it generate parent rows before children, respecting deferrable constraints?5
Data‑type fidelityDoes it produce valid Oracle DATE/TIMESTAMP WITH TIME ZONE, INTERVAL, BINARY_FLOAT, etc.?4
Masking / privacyBuilt‑in PII masking, deterministic shuffling, or integration with Oracle Data Masking Pack?4
Volume controlAbility to specify row counts per table, per partition, or per business scenario?4
ExtensibilityCustom PL/SQL generators, Java/Python hooks, or SQL scripts for complex columns?3
CI/CD integrationCLI, Docker image, or API that can be invoked from pipelines?4
PerformanceBulk‑load (direct‑path) vs. row‑by‑row; parallelism support?3
Cost & licensingOpen‑source, free tier, or enterprise license?2
Community / supportActive issues, documentation, examples for Oracle?2

Score each candidate (e.g., DBMS_RANDOM + custom PL/SQL, Oracle SQL Developer Data Modeler, Redgate SQL Data Generator, Datagen, QA3 Test Data Generator) against the table. The highest‑scoring approach usually combines a schema‑aware engine with a CI‑friendly CLI.

Tip: If you already own Oracle Enterprise Edition, the DBMS_DATAPUMP + DBMS_METADATA combo can export a production subset, then a masking script can sanitize it. This is often the fastest path for “production‑like” volumes.


4. End‑to‑End Workflow

1. Capture schema snapshot  → 2. Define data contracts  → 3. Choose generator
4. Write/configure generation scripts  → 5. Run in isolated schema
6. Validate constraints & distribution  → 7. Publish artifacts (SQL, CSV, dump)
8. Consume in test pipelines

4.1 Capture Schema Snapshot

-- Export DDL for the target schema (run as DBA or schema owner)
SET LONG 2000000
SET PAGESIZE 0
SPOOL schema_ddl.sql
SELECT DBMS_METADATA.GET_DDL('TABLE', table_name, owner)
FROM   all_tables
WHERE  owner = 'APP_OWNER';
SPOOL OFF

Store schema_ddl.sql in version control. It becomes the source of truth for every generation run.

4.2 Define Data Contracts

A data contract is a JSON/YAML file that describes what each table needs for a given test scenario.



# contracts/order_flow.yaml


tables:
  CUSTOMERS:
    rows: 5000
    columns:
      CUST_ID:          {type: identity}
      FIRST_NAME:       {type: first_name}
      LAST_NAME:        {type: last_name}
      EMAIL:            {type: email, unique: true}
      CREATED_AT:       {type: timestamp, range: "-2y..now"}
  ORDERS:
    rows: 20000
    columns:
      ORDER_ID:         {type: identity}
      CUST_ID:          {type: ref, table: CUSTOMERS, column: CUST_ID}
      ORDER_DATE:       {type: date, range: "-1y..now"}
      STATUS:           {type: choice, values: [NEW, PAID, SHIPPED, CANCELLED], weights: [0.2,0.5,0.2,0.1]}
      TOTAL_AMOUNT:     {type: numeric, precision: 10, scale: 2, min: 0, max: 5000}
  ORDER_ITEMS:
    rows: 60000
    columns:
      ITEM_ID:          {type: identity}
      ORDER_ID:         {type: ref, table: ORDERS, column: ORDER_ID}
      PRODUCT_ID:       {type: ref, table: PRODUCTS, column: PRODUCT_ID}
      QUANTITY:         {type: integer, min: 1, max: 20}
      UNIT_PRICE:       {type: numeric, precision: 8, scale: 2, min: 0.01, max: 200}

Contracts make the generation declarative, reviewable, and reusable across environments.

4.3 Choose & Configure the Generator

Below is a minimal example using QA3’s free test data generator (CLI qa3-tdg). It reads the contract, resolves references, and emits a single INSERT script that can be run with sqlplus or SQLcl.



# Install (once)


npm i -g @qa3/test-data-generator   # or download the standalone binary


# Generate


qa3-tdg generate \
  --ddl schema_ddl.sql \
  --contract contracts/order_flow.yaml \
  --output generated/order_flow_inserts.sql \
  --batch-size 5000 \
  --parallel 4

Why this tool?

  • Reads Oracle data dictionary directly (no manual mapping).
  • Handles identity columns, virtual columns, and deferred FK checks.
  • Emits direct‑path INSERT /*+ APPEND */ statements for bulk speed.
  • Free tier covers up to 1 M rows per run – enough for most functional test suites.

4.4 Run in an Isolated Schema

-- Create a clean schema for the test run
CREATE USER test_run_20240315 IDENTIFIED BY pwd DEFAULT TABLESPACE users QUOTA UNLIMITED ON users;
GRANT CREATE SESSION, CREATE TABLE, CREATE SEQUENCE, CREATE TRIGGER TO test_run_20240315;


-- Apply DDL
@schema_ddl.sql   -- run as test_run_20240315


-- Load data
@generated/order_flow_inserts.sql

Wrap the above in a shell script or a CI job step. The schema name can contain a timestamp or build number to guarantee isolation.

4.5 Validate Constraints & Distribution

Run a validation suite immediately after load. The suite can be a set of PL/SQL blocks that raise an exception if any rule fails.

-- validation/constraint_checks.sql
SET SERVEROUTPUT ON
DECLARE
  v_cnt NUMBER;
BEGIN
  -- 1. No orphan ORDER_ITEMS
  SELECT COUNT(*) INTO v_cnt
  FROM   order_items oi
  LEFT JOIN orders o ON oi.order_id = o.order_id
  WHERE  o.order_id IS NULL;
  IF v_cnt > 0 THEN
    RAISE_APPLICATION_ERROR(-20001, 'Orphan ORDER_ITEMS: '||v_cnt);
  END IF;


-- 2. Email uniqueness
  SELECT COUNT(*) INTO v_cnt
  FROM   (SELECT email, COUNT(*) c FROM customers GROUP BY email HAVING COUNT(*) > 1);
  IF v_cnt > 0 THEN
    RAISE_APPLICATION_ERROR(-20002, 'Duplicate emails found');
  END IF;


-- 3. Status distribution sanity check
  FOR rec IN (SELECT status, COUNT(*) cnt FROM orders GROUP BY status) LOOP
    DBMS_OUTPUT.PUT_LINE('STATUS '||rec.status||' = '||rec.cnt);
  END LOOP;
END;
/

Add the validation script as a post‑load gate in CI. If it fails, the pipeline stops before any test code runs.

4.6 Publish Artifacts

  • SQL script – for teams that prefer sqlplus/SQLcl.
  • Data Pump dump (.dmp) – for performance tests that need a full tablespace snapshot.
  • CSV/Parquet – for downstream analytics or non‑Oracle test harnesses.


# Example: create a Data Pump dump after load


expdp test_run_20240315/pwd@//dbhost:1521/ORCL \
  schemas=TEST_RUN_20240315 \
  directory=DATA_PUMP_DIR \
  dumpfile=test_run_20240315.dmp \
  logfile=expdp.log \
  compression=ALL

Store the dump in an artifact repository (Nexus, Artifactory, S3) with a retention policy (e.g., 30 days).

4.7 Consume in Test Pipelines



# .github/workflows/integration-test.yml


jobs:
  integration:
    runs-on: ubuntu-latest
    services:
      oracle:
        image: container-registry.oracle.com/database/free:23.3.0
        env:
          ORACLE_PWD: pwd
        ports: ['1521:1521']
    steps:
      - uses: actions/checkout@v4
      - name: Load test schema
        run: |
          sqlplus -s sys/pwd@//localhost:1521/FREEPDB1 as sysdba @ci/create_test_schema.sql
          sqlplus -s test_run_${{ github.run_id }}/pwd@//localhost:1521/FREEPDB1 @generated/order_flow_inserts.sql
      - name: Run validation
        run: sqlplus -s test_run_${{ github.run_id }}/pwd@//localhost:1521/FREEPDB1 @validation/constraint_checks.sql
      - name: Execute test suite
        run: ./run_integration_tests.sh

The pipeline now has deterministic, production‑like data for every run.


5. Worked Example: “Order‑to‑Cash” End‑to‑End Scenario

5.1 Business Requirements

RequirementData implication
Customers can have multiple addresses (billing, shipping)ADDRESSES table with ADDRESS_TYPE FK to ADDRESS_TYPES
Orders may be split across warehousesORDER_ALLOCATIONS linking ORDERS → WAREHOUSES
Payments reference a single order but can be partialPAYMENTS with ORDER_ID FK, AMOUNT ≤ ORDERS.TOTAL_AMOUNT
Invoices generated nightly, must match order totalsINVOICES derived from ORDERS + PAYMENTS

5.2 Contract Extension



# contracts/order_to_cash.yaml


tables:
  ADDRESS_TYPES:
    rows: 2
    columns:
      TYPE_CODE: {type: choice, values: [BILL, SHIP]}
      DESCRIPTION: {type: static, value: "Billing/Shipping"}
  ADDRESSES:
    rows: 12000
    columns:
      ADDRESS_ID:   {type: identity}
      CUST_ID:      {type: ref, table: CUSTOMERS, column: CUST_ID}
      TYPE_CODE:    {type: ref, table: ADDRESS_TYPES, column: TYPE_CODE}
      STREET:       {type: street_address}
      CITY:         {type: city}
      STATE:        {type: state}
      ZIP:          {type: zip_code}
  WAREHOUSES:
    rows: 5
    columns:
      WH_ID:        {type: identity}
      NAME:         {type: company}
      REGION:       {type: choice, values: [NA, EU, APAC, LATAM, MEA]}
  ORDER_ALLOCATIONS:
    rows: 25000
    columns:
      ALLOC_ID:     {type: identity}
      ORDER_ID:     {type: ref, table: ORDERS, column: ORDER_ID}
      WH_ID:        {type: ref, table: WAREHOUSES, column: WH_ID}
      QTY_ALLOCATED:{type: integer, min: 1, max: 100}
  PAYMENTS:
    rows: 18000
    columns:
      PAYMENT_ID:   {type: identity}
      ORDER_ID:     {type: ref, table: ORDERS, column: ORDER_ID}
      PAY_DATE:     {type: date, range: "-6m..now"}
      AMOUNT:       {type: numeric, precision: 10, scale: 2, min: 0.01, max: 5000}
      METHOD:       {type: choice, values: [CC, ACH, WIRE, PAYPAL], weights: [0.6,0.2,0.1,0.1]}
  INVOICES:
    rows: 20000
    columns:
      INVOICE_ID:   {type: identity}
      ORDER_ID:     {type: ref, table: ORDERS, column: ORDER_ID}
      INV_DATE:     {type: date, range: "-6m..now"}
      TOTAL_AMOUNT: {type: expr, sql: "SELECT NVL(SUM(AMOUNT),0) FROM PAYMENTS WHERE ORDER_ID = :ORDER_ID"}

Notes

  • INVOICES.TOTAL_AMOUNT uses an expression that runs a correlated sub‑query at generation time. The generator evaluates it row‑by‑row (or in batches) to keep the invoice total consistent with payments.
  • ADDRESS_TYPES is a tiny reference table; the generator can seed it once and reuse across runs.

5.3 Generation Run

qa3-tdg generate \
  --ddl schema_ddl.sql \
  --contract contracts/order_to_cash.yaml \
  --output generated/order_to_cash_inserts.sql \
  --batch-size 10000 \
  --parallel 8 \
  --seed 20240315   # deterministic run

The --seed flag guarantees the same data set for a given contract version – essential for regression comparison.

5.4 Validation Checks Specific to This Scenario

| Check |

Read more

BigQuery Test Data Generation for Data Pipeline Tests

A practical guide to “BigQuery Test Data Generation for Data Pipeline Tests,” with worked scenarios, tool considerations, validation checks, and actionable advice for QA teams.

Snowflake Test Data Generation for Analytics QA

A practical guide to “Snowflake Test Data Generation for Analytics QA,” with worked scenarios, tool considerations, validation checks, and actionable advice for QA teams.

Redis Test Data Setup for Cache and Session Testing

A step-by-step guide to “Redis Test Data Setup for Cache and Session Testing,” covering prerequisites, implementation choices, validation, and common failure modes.