PRACTICAL GUIDE / ETL testing tutorial

ETL Testing Tutorial: Validate Data Pipelines End to End

ETL testing tutorial for QA teams covering source to target validation, transformations, reconciliation, data quality, SQL checks, and defects.

By The Testing AcademyUpdated July 10, 20268 min read
All field guides
In this guide9 sections
  1. Freeze the mapping and the load contract
  2. Build a diagnostic source dataset
  3. Establish pre-load controls
  4. Reconcile completeness by disposition
  5. Validate transformations with expected-value queries
  6. Test incremental loads and corrections
  7. Verify dimensions and late-arriving relationships
  8. Inspect rejects, schema drift, and observability
  9. Report a release decision from reconciled evidence

What you will learn

  • Freeze the mapping and the load contract
  • Build a diagnostic source dataset
  • Establish pre-load controls
  • Reconcile completeness by disposition

A revenue dashboard was short by 1.7 million cents after a routine load. The pipeline was green, source and target row counts matched, and no records were rejected. The defect was a currency join that used the transaction date for most rows but the load date for late arrivals. Counts proved movement; only business-level reconciliation exposed changed meaning.

ETL testing must establish lineage from a source fact through transformation to a target measure. The examples below use PostgreSQL-compatible SQL and a small retail pipeline. Adapt date functions and hashing for a different warehouse engine.

Freeze the mapping and the load contract

Before running a pipeline, obtain the source-to-target mapping, schedule, time zone, extraction watermark, reject policy, key strategy, and transformation rules. Resolve vague statements such as “latest exchange rate” into an exact effective-date rule.

For a sales fact load, a reviewable mapping might contain:

SourceTargetRule
orders.order_idfact_sales.order_keystable source key
orders.ordered_atsale_date_keybusiness date in store time zone
order_lines.net_minornet_local_minorsum accepted lines
currency plus business datefx_raterate effective on transaction date
local amount and ratenet_usd_minordocumented rounding once at order grain
missing rateetl_rejectsreject with reason, do not default

Mark the grain of every table. “One row per sale” is not enough if a sale can split by item, shipment, or tender. Grain errors produce plausible duplicate totals and make reconciliation queries misleading.

Build a diagnostic source dataset

Random production-like volume is useful later. Begin with a small dataset where each row has a reason to exist. Include a normal same-day order, a late arrival, duplicate source delivery, null optional field, unmapped currency, cancelled order, boundary timestamp, and corrected record.

OrderBusiness caseExpected target
ETL-101EUR, same-day arrivalone fact with transaction-date rate
ETL-102EUR, three days lateone fact with original-date rate
ETL-103duplicate delivery of ETL-101no additional fact
ETL-104currency ZZZreject with FX_RATE_MISSING
ETL-105cancelled before cutoffexcluded and audited
ETL-10623:59:59 store local timecorrect store date key

Store expected values independently of target code. If the expected spreadsheet repeats the implementation formula, both can share the same defect. Manually calculate a few sentinel rows from the signed-off rule and keep their derivation with the test evidence.

Establish pre-load controls

Record source counts and control totals inside the extraction window before the run. Counts alone cannot detect a missing £100 row replaced by an extra £20 row. Use multiple measures: row count, distinct business keys, null counts, minimum and maximum timestamps, and monetary sums by currency.

SQL
SELECT currency_code,
       count(*) AS source_rows,
       count(DISTINCT order_id) AS distinct_orders,
       sum(net_minor) AS net_minor,
       min(updated_at) AS first_update,
       max(updated_at) AS last_update
FROM stage_orders
WHERE updated_at >= TIMESTAMPTZ '2026-07-09 02:00:00+00'
  AND updated_at <  TIMESTAMPTZ '2026-07-10 02:00:00+00'
GROUP BY currency_code
ORDER BY currency_code;

This PostgreSQL sql query creates pre-load controls for a half-open watermark window. The upper boundary belongs to the next batch. Save the result with the batch ID so a later rerun can be compared to the same source snapshot.

Reconcile completeness by disposition

Every extracted row should have a disposition: inserted, updated, intentionally filtered, rejected, or held for later processing. A target count that equals source count can still hide both a duplicate and an omission.

Create a reconciliation equation at business-key grain:

Example
distinct extracted keys
= inserted keys
 + updated existing keys
 + policy-filtered keys
 + rejected keys
 + deferred keys

The text block is an accounting rule, not executable code. The categories must be mutually exclusive and traceable by batch ID. If one source key appears in both “updated” and “rejected,” the operational metadata is unreliable.

Use an anti-join to find unexplained keys:

SQL
SELECT s.order_id
FROM stage_orders s
LEFT JOIN fact_sales f
  ON f.source_order_id = s.order_id
LEFT JOIN etl_rejects r
  ON r.source_key = s.order_id AND r.batch_id = 'B20260710'
WHERE s.batch_id = 'B20260710'
  AND f.source_order_id IS NULL
  AND r.source_key IS NULL;

The sql query should return zero rows after filtered and deferred keys are represented in their own auditable tables or added to the comparison. Returned IDs are missing dispositions, not automatically missing facts.

Validate transformations with expected-value queries

Test transformations at the level where the rule applies. For currency conversion, join the rate by currency and transaction date, aggregate at order grain, round once as specified, then compare with target values. Do not reuse the target table’s stored rate to compute the expected result.

SQL
WITH expected AS (
  SELECT s.order_id,
         r.rate_to_usd,
         round(sum(s.net_minor) * r.rate_to_usd)::bigint AS expected_usd_minor
  FROM stage_order_lines s
  JOIN fx_rates r
    ON r.currency_code = s.currency_code
   AND r.effective_date = (s.ordered_at AT TIME ZONE s.store_timezone)::date
  WHERE s.batch_id = 'B20260710'
  GROUP BY s.order_id, r.rate_to_usd
)
SELECT e.order_id, e.rate_to_usd, e.expected_usd_minor,
       f.fx_rate, f.net_usd_minor
FROM expected e
JOIN fact_sales f ON f.source_order_id = e.order_id
WHERE e.expected_usd_minor <> f.net_usd_minor
   OR e.rate_to_usd <> f.fx_rate;

This PostgreSQL sql query should return zero rows. A row with matching rates but different amounts indicates grain or rounding logic; a rate difference points to effective-date selection.

Also test trimming, code mappings, Unicode, default values, derived dates, precision, and null semantics. An empty string converted to zero is not the same as unknown. Expected outcomes must state whether the pipeline rejects, defaults, or preserves missing values.

Test incremental loads and corrections

Full loads hide watermark defects. Run consecutive batches with controlled timestamps. Place records one microsecond before the boundary, exactly on it, and just after it. Verify the half-open interval neither loses nor repeats the boundary record.

Then update an existing source order without changing its creation time. The target should update if extraction is based on updated_at. Deliver the same change twice and confirm idempotency. Simulate a failed batch after staging but before publish, restart it, and check that facts, rejects, and audit counts remain correct.

For a corrected transaction, define whether history is overwritten, versioned, or compensated. A fact table that silently overwrites a value can make yesterday’s published report impossible to reproduce. QA must validate the chosen history policy, not impose one.

Verify dimensions and late-arriving relationships

Dimension processing needs separate tests for new members, attribute changes, unknown members, and facts arriving before their dimension. For a type 2 customer dimension, an attribute change should close the old row and create a new current row without overlapping validity.

SQL
SELECT customer_source_id
FROM dim_customer
GROUP BY customer_source_id
HAVING count(*) FILTER (WHERE is_current) <> 1
   OR max(valid_from) > max(COALESCE(valid_to, valid_from));

The PostgreSQL sql example is a quick current-row control, but it is not a full overlap detector. Reviewers should expect zero rows, then add a self-join interval check for the warehouse’s exact inclusive or exclusive validity convention.

When a fact arrives before its customer dimension, verify the specified behavior: use an unknown surrogate temporarily, defer the fact, or create an inferred member. Then load the dimension and confirm the repair process updates the relationship without duplicating the fact.

Inspect rejects, schema drift, and observability

Reject testing is successful only if bad data is visible and recoverable. Validate reason code, source key, safe payload, batch ID, detection timestamp, retry eligibility, and final disposition. Correct the missing ZZZ exchange rate, replay ETL-104, and prove the reject closes while exactly one fact appears.

Introduce controlled schema changes in a test source: an added nullable column, renamed field, wider value, and changed type. The pipeline should follow its contract, whether that means accepting a compatible addition or stopping before publishing partial data. A green orchestration task with null-filled targets is a failure.

Dashboards should expose extracted, loaded, updated, rejected, deferred, and filtered counts plus watermark and batch duration. Alerts need actionable identifiers. “Load failed” without batch, stage, and cause lengthens recovery.

Report a release decision from reconciled evidence

The final evidence pack should identify mapping version, code version, batch IDs, source snapshot window, time zones, diagnostic dataset, reconciliation outputs, reject replay, and known exclusions. Retain actual mismatch rows, not only screenshots of green counts.

Approve when every extracted key has one valid disposition, sentinel transformations match independent calculations, incremental boundaries and restarts are safe, dimension history follows policy, rejects are traceable, and downstream totals reconcile at their declared grain. Pipeline completion is an operational signal. ETL quality is proof that business meaning survived the trip.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 10, 2026 / Reviewed July 10, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    HTTP Semantics

    IETF

    The normative semantics for HTTP methods, status codes, and fields.

FAQ / QUICK ANSWERS

Questions testers ask

What is ETL testing?

ETL testing verifies that data extracted from source systems is transformed according to business rules and loaded correctly into the target system, warehouse, lake, or reporting layer. It checks completeness, accuracy, duplicates, rejects, transformations, schedules, performance, and reconciliation.

What is source to target testing?

Source to target testing compares data from the original source with data loaded into the destination. QA verifies field mapping, transformation rules, data types, counts, filters, joins, derived values, rejected records, and audit metadata to confirm that the pipeline preserved business meaning.

Which SQL skills are needed for ETL testing?

ETL testers need SELECT, JOIN, GROUP BY, aggregate functions, CASE expressions, date functions, NULL handling, window functions for duplicates, and reconciliation queries. They also need to understand source keys, target keys, slowly changing dimensions, and load timestamps.

Can ETL testing be automated?

Yes. Count checks, checksum checks, source to target comparisons, transformation rules, duplicate detection, schema drift checks, and data quality rules are strong automation candidates. Manual review is still useful for new mappings, ambiguous business rules, and defect investigation.

What defects are common in ETL testing?

Common ETL defects include missing rows, duplicate rows, wrong joins, incorrect filters, bad date conversions, truncation, precision loss, wrong currency conversion, failed incremental loads, late arriving data issues, rejected records, and report totals that do not reconcile.