dbt tests are assertions about your data that run in the warehouse and fail the build when broken — catching bad data before dashboards do. Plus auto-generated docs and a lineage graph.
dbt ships four generic tests that catch the most common data problems declaratively: not_null, unique, accepted_values, and relationships (referential integrity). You attach them to columns in YAML, and dbt compiles each into a query that should return zero rows — any rows mean a failure. This is how you guarantee a primary key is actually unique or a foreign key actually resolves.
# models/marts/_marts.yml
version: 2
models:
- name: fct_orders
columns:
- name: order_id
tests:
- unique # no duplicate order ids
- not_null # every row has one
- name: status
tests:
- accepted_values:
values: ['placed', 'shipped', 'completed', 'returned']
- name: customer_id
tests:
- relationships: # every customer_id exists in dim_customers
to: ref('dim_customers')
field: customer_idUnder the hood every dbt test is just SQL that selects the failing rows; if it returns any, the test fails. This means custom, business-specific tests are trivial — a singular test is a .sql file that queries for what should never exist. You can assert anything your SQL can express: no negative amounts, no orders before the customer signed up, no future dates.
-- tests/assert_no_negative_amounts.sql
-- a "singular" test: any rows returned = failure
select order_id, amount
from {{ ref('fct_orders') }}
where amount < 0
-- run it: dbt test --select fct_orders
-- a red test blocks the build (and, in CI, the merge) BEFORE the bad
-- number reaches a dashboard. This is data quality as code.Descriptions you write in YAML, plus the DAG dbt already knows, generate a browsable docs site with a searchable catalog and an interactive lineage graph. Because the docs are built from the same files as the models, they cannot drift out of date the way a wiki does. Anyone can answer “what is this column and where does it come from?” without asking you.
# describe models + columns right next to their tests
models:
- name: fct_orders
description: "One row per order. Grain: order. Source: jaffle_shop.orders."
columns:
- name: amount
description: "Order total in USD (converted from cents)."
# generate + serve the docs site (catalog + lineage graph):
# dbt docs generate
# dbt docs serve
# docs are built from the SAME files as the code -> they can't go stale.The value of tests is realized only when they run automatically. dbt build interleaves models and their tests in DAG order, and stopping on failures prevents bad data from propagating downstream. In the deployment lesson you wire this into CI so no change merges while a data test is red — the same shift-left idea as software CI.
dbt build # runs models AND their tests together, in DAG order:
build stg_orders -> test stg_orders -> build fct_orders -> test ...
Fail-fast options:
--fail-fast stop on the first failure
stg test fails -> downstream models are skipped (no bad data built)
Local: run 'dbt build' before you push.
CI: run 'dbt build --select state:modified+' on every PR (deployment lesson).
A red data test should block the merge, exactly like a failing unit test.