Data Engineering  /  dbt

๐Ÿ”„ dbt โ€” Data Build Tool 60 guides ยท updated 2026

Analytics engineering with SQL โ€” models, tests, sources, and Jinja macros that turn raw warehouse tables into trustworthy, documented data products.

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:

TypeDefinedBest for
Generic (schema) testsYAML, applied to a column or modelCommon, reusable checks โ€” uniqueness, nulls, referential integrity
Singular testsA standalone SQL fileOne-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/staging/_stg_orders.yml
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_id

Four 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

Terminal window
# Run every test in the project
dbt test
# Run tests for a specific model only
dbt test --select stg_orders
# Run tests for a model and everything downstream of it
dbt 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:

Terminal window
dbt show --select unique_stg_orders_order_id

Or 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: warn

A 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.

Terminal window
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

ConceptPurpose
Generic testsReusable, YAML-declared checks (unique, not_null, etc.)
Singular testsCustom SQL for business-logic-specific validation
severity: warnNon-blocking test failures, tracked but not fatal
dbt buildTests 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.