A Data Test Runs on Rows You Did Not Choose
real Jinja, real macros, real adapter.dispatch, real type coercion, real compute, real cost.
Every test in Chapter 7 shares one shape: a compiled anti-query run against rows that already exist in the warehouse, whose returned row count decides the verdict. That means each of them can only ever judge the rows a model actually produced — which is why ep_1008’s implausible 1,980ms was a coverage fact, not a bug in the suite.
unit_tests: inverts every part of that. dbt takes the model’s real SQL, replaces every ref()/source() with a CTE built from rows you write, runs the whole thing for real, and compares the result to rows you also write. Operate two dials below before reading one more word about how.
A data test runs on rows you did not choose. A unit test runs on rows you did. Everything downstream of that one substitution is still real: real Jinja, real macros, real adapter.dispatch, real type coercion, real compute, real cost.
Confusing “the tests are green” with “nothing here connects to the warehouse” is easy precisely because the fixture data is fake while everything around it — the connection, the relations, the compute — is real. That gap between what looks fake and what is actually real is exactly what makes “where do unit tests earn their keep” in Section 8 a real question instead of a rhetorical one.
- unit_tests:
- A top-level YAML key in a properties file under models/ — native since dbt Core 1.8, current in 1.12.0. given supplies fixture rows per input; expect supplies the rows the compiled model should produce.
- given / expect
- given: a list of {input, rows} pairs, one per ref()/source() the model calls. expect: the rows (and only the columns) the model should produce from them.
- No arguments: key
- Chapter 7 established test arguments under arguments: and behavior under config:. unit_tests: has no arguments: key at all — it is a different schema, not a stricter version of the same one.
- Still a warehouse connection
- No table data is read, but a live connection and real compute are required — direct parents must physically exist as relations, and fixture column types are read from them.
- --exclude-resource-type unit_test
- dbt’s documented way to skip unit tests in prod and keep the compute: dbt build --exclude-resource-type unit_test, or the DBT_EXCLUDE_RESOURCE_TYPES env var for CI without touching the invocation. Applies to dbt build, dbt test, dbt list, and dbt clone alike.
Reading “no real rows are read” as “this runs offline.” Chapter 7’s own handoff line — “before a single real row ever reaches it” — is true about data and gets misread as true about compute. A live connection, real relations, and real queries are still required; only the row-level data is fixture-supplied.
Stubbing a dispatched macro’s adapter-specific arm — e.g. snowflake__local_day() — with overrides.macros to force a unit test through. overrides.macros only replaces macros the model calls by name; a dispatched call like local_day() (Chapter 6) resolves to its adapter arm at compile time, which overrides.macros cannot see or intercept (dbt-core #9624). Reserve overrides.macros for the non-deterministic and environment-dependent — run_started_at, current_timestamp — never for the logic under test. If you are stubbing your own business macro, you have written a test of nothing.
Before writing a unit test, name which of the two dials the defect you are worried about actually depends on — the rows in the table, or the model’s own code.
Read “unit tests connect to the warehouse” as a cost fact, not a contradiction — dbt build --exclude-resource-type unit_test (or DBT_EXCLUDE_RESOURCE_TYPES in CI) is the documented way to skip that cost in prod.
Keep Chapter 7’s arguments:/config: shape and this chapter’s given/expect shape mentally separate — reaching for the wrong one produces a compile error, not a subtle bug, so it is cheap to catch but easy to make on a first unit test.
Every test in Chapter 7 is a compiled anti-query run against rows that already exist in the warehouse, judged purely by the row count it returns. A unit test keeps the “compiled and run for real” part and replaces the rows with fixtures you write — a different mechanism, not a stricter version of the same one.
A data test is blind to the model’s code and sees only its rows; a unit test is blind to the table’s rows and sees only the model’s code — hold that inversion and every trap in this chapter follows from it.
