DBTBuilding a Project

Tests: Generic, Singular, and Custom

How analytics engineers turn raw warehouse tables into trusted models — pick a topic on the left and its full breakdown loads here: the mental model, the compiled SQL dbt actually issues, live runs you can drive yourself, and the failure modes that quietly ship wrong numbers.

18 min readTopics chapter readerLevel · Medium
01 · The opening beat

A Test Is a Query That Should Return Nothing

A generic test is a parameterized anti-query — SQL written to find what should not exist. dbt compiles it, counts the rows it returns, and compares that count to a threshold; the comparison, not the rows themselves, is the entire verdict. Hold onto that one sentence — every trap in this chapter is a consequence of it.

4 min · Topic 1 of 9

stg_episodes carries 9 physical rows behind 8 distinct episode_id values — that gap is this chapter’s opening beat, and every count from here on respects it. A dbt test is not a constraint the warehouse enforces; it is a SELECT dbt expects to return zero rows, wrapped in a counting query that turns “zero rows” into a pass/fail verdict.

A handful of new tests ship on this project by the end of the chapter. The honest fact this section teaches first: having more tests is not the same as having coverage. The map below starts exactly where most real projects start — pick a node, then find out what one missing test actually costs.

16 nodes, the same Wavelength project since Chapter 5 — 8 of the 11 models already carry a uniqueness badge; three carry nothing at all. Pick a node, then toggle the one relationship this project is missing.
source
staging
intermediate
mart
stg_listensCurrent tests: unique(listen_id). That is uniqueness on the key — nothing here checks completeness, referential integrity, an accepted range, or a value list.
The missing relationship
Off — 5 nodes are exposed to an orphan episode_id, silentlyAn orphan episode_id in stg_listens has no test in its path — it reaches int_listens_enriched, fct_listens, agg_daily_listens, and mart_engagement_summary with nothing to stop it. Only fct_listens among them even has a test of its own — and a uniqueness test on listen_id says nothing about an episode_id that points nowhere.
Core mental model

A generic test is a parameterized anti-query — SQL written to find what should not exist. dbt compiles it, counts the rows it returns, and compares that count to a threshold; the comparison, not the rows themselves, is the entire verdict. Hold onto that one sentence — every trap in this chapter is a consequence of it.

Why it matters

Every staging model and most marts already carry a uniqueness test on their key — the easy 80% every team ships in week one. That’s also why “we have tests” and “we have coverage” get used interchangeably when they describe completely different things: one counts YAML lines, the other counts what could go wrong that nothing would catch. This chapter spends its first widget making that gap visible before naming a single built-in.

Data test
dbt’s term for what this chapter covers — a compiled SELECT whose row count decides pass/warn/fail.
Anti-query
A query written to find what should not exist, not what you want — every built-in and every singular test in this chapter is one.
Generic test
A parameterized anti-query — a macro taking model and column_name — reusable across every column and model it is pointed at.
Coverage
Which columns, relationships, and value ranges a project actually asserts — not how many data_tests: lines exist in YAML.
Common mistake

Reading “every model has a test” as “the project is tested.” A uniqueness test on a primary key says nothing about completeness, referential integrity, accepted values, or plausibility — a project can pass every test it has and still ship silently wrong numbers on data no test ever looked at.

Better habit

Before adding a tenth test, ask which data-quality dimension the project has zero coverage on — not which model has zero tests.

Read a green dbt build the same way you’d read a passing suite with 40% code coverage: real, and incomplete.

Treat the coverage map as a living artifact, not a one-time audit — a new mart with no test on its grain is invisible until you go looking.

Four things covered, one thing not

The four built-ins this chapter opens with cover uniqueness, completeness (wherever you point not_null), and part of validity (value lists, ranges) — and nothing of accuracy. The six dimensions data quality gets scored against, and what closes that last gap, live in the Data Pipeline KB.

Data quality & validation — the six dimensions (Data Pipeline KB)
Remember this

Coverage is a property of the DAG and the defect classes tested, not a count of data_tests: lines — the next seven sections close specific, named gaps in it, on purpose.