A Test Is a Query
Concepts covered: Data tests
A dbt test is a SELECT that returns the rows breaking a rule. dbt sends the query, counts the rows that come back, and prints PASS when the count is 0 and FAIL followed by the count when it is not. The 4 built-in tests, and every test anyone writes later, work this one way, so reading a test comes down to reading its query and asking which rows it would return from the table as it stands today.
One query, one count
Take a model stg_orders holding 5 rows, 1 of which arrived without an order_id because the checkout service logged the row before it assigned the id. The rule is that every order carries an id. A test for that rule selects the rows that have none, and nothing else.
| order_id | customer_id | status | amount |
|---|---|---|---|
| 101 | 1 | shipped | 40 |
| 102 | 2 | placed | 25 |
| null | 1 | placed | 60 |
| 104 | 3 | delivered | 15 |
| 105 | 2 | shipped | 30 |
select
order_id
from analytics.stg_orders
where order_id is nullAgainst that table the query returns 1 row, the one with the missing id, so dbt prints FAIL 1. Once the row is dropped from the model or repaired in the source, the next run of the same query returns 0 rows, which dbt prints as PASS. The number after FAIL is the count of rows the test query returned. For this query each returned row is 1 bad row of the model, so FAIL 4000 would mean 4,000 orders without an id; section 1 shows tests whose query returns 1 row per bad value, where a single returned row can stand for many bad rows.
What dbt wraps around it
dbt never pulls the failing rows back to the machine running it. It wraps the test query in an outer SELECT that counts them, so the warehouse answers with 1 row holding a number whether the test finds 0 bad rows or 10 million.
select
count(*) as failures,
count(*) != 0 as should_warn,
count(*) != 0 as should_error
from (
select
order_id
from analytics.stg_orders
where order_id is null
) dbt_internal_testFor the 5-row table above, the inner query finds the 1 order without an id, and the warehouse sends back this single row:
| failures | should_warn | should_error |
|---|---|---|
| 1 | true | true |
The inner query is the test. The outer query is dbt's way of asking the warehouse for a number instead of a result set, and should_error is the flag it reads: true prints FAIL, false prints PASS. A test writes nothing to the warehouse, which is why dbt test can run against a production schema without changing a table in it.
The query describes the bad rows
The query names the rows that should not exist, which is the reverse of how most people write a first query. Written the other way round, where order_id is not null returns 4 rows from the table above and would return every row from a healthy table, so the test would fail hardest when the data is at its best. A test that keeps failing with a count equal to the model's row count is almost always selecting the good rows.
The same shape fits any rule. An order amount must be 0 or more, so the test selects orders whose amount is below 0; a shipped order must carry a shipping date, so the test selects shipped orders where that date is null. Each query reads as a sentence that begins with find me the rows where, and the rest of the sentence is the mistake.
| Rows the query returns | What dbt prints | What it means |
|---|---|---|
| 0 | PASS | the rule holds for every row |
| 1 | FAIL 1 | 1 row breaks the rule |
| 250 | FAIL 250 | 250 rows break the rule |
Running every test
dbt test runs every test in the project, 1 query each, printing a line per test with the outcome and the count, and closes with a summary line that totals the outcomes. It exits with a non-zero code when any test failed, so a script that called it can tell the run went wrong without reading the log. The same lines appear inside dbt build, where each test runs as soon as its model is built, which section 3 returns to.
About this lesson
This section is part of the Testing: Beginner lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.