## TL;DR
Test in layers: unit-test transform functions on small fixtures, run the full pipeline in staging on sampled or synthetic data, and embed data-quality assertions (row-count bounds, null rates, schema checks) as pipeline steps. A staging run with production-like data shape catches the vast majority of issues, because data pipelines fail on data shape, not just code logic.

```text
how to test data pipelines before production
```

## Use this when
- Setting up testing for a new or existing ETL pipeline
- Production keeps catching bugs that testing should have caught
- You need a staging strategy that isnt just "run it and eyeball it"

## Not for this skill when
- Testing the orchestrator or scheduler itself
- Load or performance testing
- Monitoring data quality in production (separate concern)

## Steps

1. Unit-test transform functions with tiny, hand-built fixtures:

```python
def test_normalize_amount():
    assert normalize_amount("$1,234.50") == 1234.50
    assert normalize_amount(None) is None
```
Expected output: green tests that run in milliseconds. This catches logic bugs in the functions where most transformation code actually lives.

2. Contract-test your sources: assert the schema and sample rows you depend on:

```python
def test_source_schema():
    cols = get_columns("staging.orders")
    assert {"order_id", "amount", "created_at"}.issubset(set(cols))
```
Expected output: the test fails the moment an upstream change breaks your assumptions, before the pipeline runs at all.

3. Run the full pipeline in staging on a realistic sample:

```shell
# staging run: 1 percent sample, or one recent day of data
run_pipeline(env="staging", sample="2026-10-03")
```
Expected output: the pipeline completes end to end and the outputs look sane on inspection. Shape-realistic data matters more than volume here.

4. Embed data-quality assertions as pipeline steps, not as an afterthought:

```sql
-- fail the run if the output looks wrong
SELECT CASE WHEN COUNT(*) = 0 THEN 1/0 END FROM marts.orders;
SELECT CASE WHEN COUNT(*) FILTER (WHERE order_id IS NULL) > 0 THEN 1/0 END FROM marts.orders;
```
Expected output: the pipeline fails fast on empty outputs or null keys instead of publishing bad data. These assertions then protect every future run, not just the staging one.

5. Test the backfill path too, not just the incremental one:

```shell
run_pipeline(env="staging", backfill_start="2026-09-01", backfill_end="2026-09-07")
```
Expected output: the backfill completes without double-counting or key collisions. Backfills exercise different code paths (full refresh, window overlaps) and break independently of incremental runs.

## Variant phrasings

### how to test etl pipeline
Layers: unit tests for logic, contract tests for sources, staging runs for integration, assertions for ongoing protection. Skip any layer and that class of bug reaches production.

### data pipeline testing strategy
Treat the pipeline like software: CI runs the unit and contract tests on every change, staging runs validate the full flow, and in-pipeline assertions are the runtime safety net. The data fixtures are the part most teams underinvest in.

### staging environment for data pipelines
Staging needs production-like shape (same schemas, realistic distributions, edge cases present) more than production-like volume. A 1 percent sample with the weird rows included beats a full copy of clean data.

## Why it happens
Data pipelines fail on data shape: unexpected nulls, new enum values, type changes, skewed partitions. Code review cant see those, and unit tests on hand-built fixtures dont include them. Only running the real transforms against realistic data surfaces shape bugs, which is why the staging run is the highest-value layer. The assertions then convert one-time testing into permanent protection.

## Edge cases
- PII in staging is a real risk; use synthetic or masked data, and make sure the masking preserves the shapes your tests depend on.
- Sampling can hide skew: if the bug only appears in one partition or one source, a random sample may miss it. Include targeted edge-case rows.
- Staging infrastructure drifts from prod (smaller warehouse, different config); performance bugs wont reproduce there.
- Tests that hit real sources in CI are flaky by nature; prefer recorded fixtures for unit tests and live sources only in scheduled staging runs.
- Assertion thresholds need tuning: too tight and every normal variation pages you; too loose and they catch nothing. Start from historical distributions.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_Fq2oOsAZLdmeq_UEMLp9RA
