dbt Source Testing: Validating Raw Data Before It Enters Your Pipeline
Most teams test their models thoroughly and never think to test their sources at all — which is backwards, given that every model built downstream inherits whatever problems exist in the raw data. A duplicate key in a source table doesn’t stay contained; it propagates through every join and aggregation that touches it. Testing sources directly catches upstream data problems at the earliest point they can be caught, before your own transformation logic has had a chance to amplify them.
Tests on Sources Look Exactly Like Tests on Models
The same generic tests covered in Generic Tests — unique, not_null, accepted_values, relationships — attach to source columns in sources.yml with identical syntax to a model’s schema file.
sources: - name: raw schema: raw_app_data tables: - name: orders columns: - name: id tests: - unique - not_null - name: status tests: - accepted_values: values: ['pending', 'shipped', 'delivered', 'cancelled'] - name: customers columns: - name: id tests: - unique - not_null - name: email tests: - not_nullRunning dbt test --select source:raw executes only these source-level tests, isolated from model tests — useful as a fast, early check in a pipeline before any transformation work begins.
Why This Matters More Than It Looks Like It Should
Consider a raw orders table with a duplicate id — perhaps a loader retried a failed sync and inserted the same batch twice. Without a source test, that duplicate flows silently into your staging model, then into every join downstream. Revenue gets double-counted. Order counts get inflated. By the time someone notices the dashboard numbers look wrong, you’re debugging four layers away from where the actual problem originated.
A unique test directly on the source catches this in seconds, at the exact point the bad data entered the project — before a single downstream model has run.
Combining Source Freshness With Source Tests
Source testing and source freshness (covered in Source Freshness) are complementary, not the same thing. Freshness asks “is this data recent enough?” Testing asks “is this data structurally correct?” A production pipeline benefits from running both, typically freshness first:
dbt source freshnessdbt test --select source:*dbt buildIf a source is stale, there’s often little point running structural tests on it yet — but both checks running before the main build means you catch upstream problems before spending compute transforming data you already know is compromised.
Testing Relationships Between Sources
relationships tests aren’t limited to checking a model column against another model — they work equally well checking one source against another, catching sync inconsistencies between systems that are supposed to stay aligned.
sources: - name: raw tables: - name: order_items columns: - name: order_id tests: - relationships: to: source('raw', 'orders') field: idThis catches a specific and common failure mode in multi-table syncs: order_items finished loading before orders did, or a partial sync dropped some parent records, leaving orphaned line items that reference orders which technically don’t exist yet in your raw layer.
A Practical Source Testing Checklist
For any new source, a reasonable minimum bar before building anything downstream of it:
| Check | Why |
|---|---|
unique + not_null on the primary key | Catches duplicate/missing rows at the earliest layer |
not_null on foreign keys | Ensures joins downstream won’t silently drop unmatched rows |
accepted_values on status/category columns | Catches new enum values before they break downstream logic |
freshness config | Ensures the source is actually current before building on it |
Source Tests in CI
Because source tests run against the same physical raw tables regardless of which dbt environment triggered them, they’re a natural fit for CI pipelines that want a fast early signal — testing sources takes seconds and requires no model builds at all, making it a cheap first gate before running the (often much more expensive) full model build and test suite.
# Example CI step ordering (pseudocode)- run: dbt deps- run: dbt source freshness- run: dbt test --select source:*- run: dbt buildA failure at the source-testing stage stops the pipeline before any compute is spent transforming data that’s already known to be broken — a meaningful cost and time saving on large projects.
Common Mistakes
Only testing models, never sources. This is the most common gap — teams invest heavily in model-level tests while raw tables go completely unvalidated, missing the earliest and cheapest point to catch a problem.
Assuming the loading tool guarantees data quality. ELT tools like Fivetran or Airbyte move data reliably, but they don’t validate business rules or guarantee uniqueness at the destination — that’s still dbt’s job, applied to the source layer.
Not testing relationships across sources from different systems. Sync timing differences between separately-loaded systems are a common and often-overlooked source of orphaned records that only a cross-source relationships test will catch.
Summary
| Practice | Benefit |
|---|---|
| Generic tests on source columns | Catches structural problems in raw data immediately |
relationships across sources | Catches sync timing issues between separately loaded systems |
| Testing sources before building models | Fails fast and cheap, before compute is spent on bad data |
| Combining with freshness checks | Ensures data is both current and structurally correct |
Testing your sources is the cheapest, earliest insurance policy in a dbt project — catching a problem here costs a few seconds of query time; catching the same problem four layers downstream, after it’s been joined and aggregated repeatedly, costs a debugging session and a correction to every affected dashboard.