Pipeline Testing
Test the transform, and test the data it emits
Data pipelines deserve tests just like code: unit tests on transform logic, fixture-based integration tests, and data tests on outputs (uniqueness, grain, freshness). The cheap wins are the ones you run in CI before deploy.
What you will learn
- Write unit tests for transformation functions
- Build fixture-driven integration tests for models
- Add data tests (uniqueness, grain, freshness) in the pipeline
What is this?
Pipeline testing spans three levels: unit tests on pure transform logic (functions, macros, SQL expressions), integration tests that run models against small fixture tables, and data tests that assert on real output (uniqueness of grain keys, not-null, row counts, freshness). Most value comes from fast tests in CI plus data assertions at deploy time.
Why do you need it?
- Silent schema drifts and grain violations are data bugs; tests catch them before the dashboard does.
- Tests encode the contract: 'what does correct look like' becomes executable.
- CI-friendliness is the difference between tests that run and tests that rot.
Mental model
A pilot's pre-flight checklist
Pipeline tests
You don't 'hope' the straps and fuel are right — you check them before takeoff (unit/integration in CI) and verify the readings in the air (data tests on outputs).
How it works
- Unit: pure functions/macros take inputs -> expected outputs; run in seconds with pytest/JS test harness.
- Integration: a fixture (small CSV/SQL dataset) runs the real model; assert the exact output rows.
- Data tests: uniqueness, not_null, relationships, accepted_values, custom SQL run against built outputs.
- Freshness tests: assert max(updated_at) is recent, per table, so staleness alerts before consumers do.
- CI wiring: run unit + fixture integration on each PR; run data tests on deploy stage and on schedule.
Three test layers and where they run.
Key commands
pytest tests/ -qRun the unit+fixture suite fast in CI.
dbt test --select fct_order_linesRun every declared data test on that model.
dbt buildRun models then tests in dependency order, failing fast per node.
dbt source freshnessAssert sources are fresh within the SLA (staleness test).
Examples
A unit test for a transform helper
# transform.pydef normalize_amount(raw): return round(Decimal(str(raw)), 2) # test_transform.py (pytest)import pytestdef test_normalize_amount_handles_strings(): assert normalize_amount("10.005") == Decimal("10.01") def test_normalize_amount_rejects_none(): with pytest.raises(TypeError): normalize_amount(None)Fast, deterministic unit tests encode the contract of the transform before it ever touches a warehouse.
In production
- A PR that adds a column triggers fixture integration: one test proves the new column flows through.
- Grain tests log 'dup order_line_key' before the number reaches a dashboard.
- Freshness tests on raw sources alert an hour before consumers do, pointing at the real gap.
Best practices
- Keep unit tests fast and pure; never touch real warehouses in unit scope.
- Data-test every grain key (unique + not_null) and every partition boundary you model.
- Run tests in CI on PR and again on deploy; schedule a daily full test pass too.
- Treat a failing test as a contract breach: fix or formally change the contract, not the test.
Common mistakes
- Testing only happy paths, so nulls and type drift sail through.
- Data tests on outputs but zero fixture tests, so logic regressions still pass.
- Deleting a failing test to unblock a deploy without a root-cause follow-up.
Debugging tips
- A transform slot changing under test = the fixture drift; regenerating fixtures prints the intended shift.
- When a data test fails in prod but not CI, diff the environment (schema drift, permissions, catalog pointer).
- Keep a 'golden dataset' of expected outputs; a regression prints exactly which row changed.
Interview checkpoint
Quick quiz
Which test catches a filter that quietly drops half the rows?
Uniqueness and not-null tests on grain keys are data tests best run on every deploy.