dbt Tests Explained: Automated Validation for Every Model
A data pipeline that runs without errors and a data pipeline that produces correct data are two different claims, and itโs entirely possible to have the first without the second. A dbt run can complete successfully while quietly producing duplicate rows, unexpected nulls, or a join that fans out and silently inflates every downstream number. dbtโs testing framework exists to catch exactly that gap โ validating not just that models built, but that what they built is actually correct.
The Two Kinds of dbt Tests
dbt supports two fundamentally different testing approaches, and knowing when to reach for each one matters:
| Type | Defined | Best for |
|---|---|---|
| Generic (schema) tests | YAML, applied to a column or model | Common, reusable checks โ uniqueness, nulls, referential integrity |
| Singular tests | A standalone SQL file | One-off business logic specific to your data |
Generic tests are covered in depth in Generic Tests and custom SQL-based validation in Custom Tests โ this article covers how the testing system works as a whole.
Attaching Tests in YAML
The most common way tests enter a project is directly alongside a modelโs column documentation.
models: - name: stg_orders columns: - name: order_id tests: - unique - not_null - name: status tests: - accepted_values: values: ['pending', 'shipped', 'delivered', 'cancelled'] - name: customer_id tests: - relationships: to: ref('stg_customers') field: customer_idFour tests, four lines each โ no separate test file to write, no SQL to hand-craft, because these are all built-in generic tests dbt ships out of the box.
Running Tests
# Run every test in the projectdbt test
# Run tests for a specific model onlydbt test --select stg_orders
# Run tests for a model and everything downstream of itdbt test --select stg_orders+Output tells you exactly which test failed and, critically, how many rows failed it โ not just a pass/fail boolean.
1 of 4 START test not_null_stg_orders_order_id ......... [RUN]1 of 4 PASS not_null_stg_orders_order_id ................ [PASS in 0.42s]2 of 4 START test unique_stg_orders_order_id ............ [RUN]2 of 4 FAIL 3 unique_stg_orders_order_id ................ [FAIL 3 in 0.51s]A FAIL 3 means three rows violated uniqueness โ dbt runs the test as an actual SQL query counting failing rows, not a black-box check, which means you can inspect exactly which rows failed.
Inspecting Which Rows Actually Failed
Every test compiles to a query you can run directly. Finding the failing rows for debugging is one command away:
dbt show --select unique_stg_orders_order_idOr manually, by looking at the compiled test SQL under target/compiled/ and running the underlying query yourself โ tests in dbt are transparent SQL, not opaque validations, which makes debugging a failure a normal SQL investigation rather than guesswork.
Test Severity: warn vs error
Not every failed test should block a deployment. dbt supports a severity config that distinguishes โsomethingโs off, worth investigatingโ from โstop the pipeline.โ
columns: - name: discount_pct tests: - not_null: config: severity: warnA warn-severity test failure is logged and shown clearly in output, but doesnโt fail the overall dbt test command โ useful for known, tolerated data quality issues youโre actively tracking but havenโt fully fixed yet. Combined with error_if and warn_if thresholds, you can even set a tolerance:
columns: - name: discount_pct tests: - not_null: config: severity: warn warn_if: ">10" error_if: ">100"This only warns if more than 10 rows fail, and only hard-fails if more than 100 do โ a realistic middle ground between โzero toleranceโ and โno monitoring at all.โ
Tests as Part of dbt build
Running dbt run followed by dbt test as two separate steps means bad data can briefly exist in a downstream table before the test catches it. dbt build, covered in dbt build, interleaves building and testing per model in dependency order, catching a failure before it propagates further down the DAG.
dbt build --select stg_orders+If stg_orders fails its tests, dbt build stops before building anything downstream of it โ a meaningfully safer default than running the full DAG and only discovering the problem afterward.
Tests on Sources, Not Just Models
Tests arenโt limited to models you build โ they can (and should) be attached to sources too, validating the raw data itself before your transformations even touch it. This is explored fully in Source Testing, and itโs often more valuable than testing your own models, since it catches upstream problems at the earliest possible point.
Common Mistakes
Only testing final mart models. A test failure three layers downstream is much harder to trace back to its root cause than the same test attached at the staging layer, where the bad data first entered the project.
Treating every failure as equally severe. Blanket error severity on every test means the team either fixes everything immediately or starts ignoring failed builds altogether. Calibrated warn/error thresholds keep the signal meaningful.
Not running tests in CI. A model that passes tests locally but was never tested against the CI/staging build can still ship a broken assumption to production undetected โ testing needs to be part of the pipeline, not just a local habit.
Summary
| Concept | Purpose |
|---|---|
| Generic tests | Reusable, YAML-declared checks (unique, not_null, etc.) |
| Singular tests | Custom SQL for business-logic-specific validation |
severity: warn | Non-blocking test failures, tracked but not fatal |
dbt build | Tests models in dependency order, stopping propagation of bad data |
dbt tests turn โI think this data is correctโ into a verifiable, automated, re-runnable claim. A project without meaningful test coverage isnโt actually more reliable than one with it โ it just hasnโt found its bugs yet.