Failing Tests Stop Children

Concepts covered: Test severity and blocking

dbt build runs each model and then the tests declared on it before it moves to anything that reads from that model. A test that fails stops the build from running the models downstream of the tested model, and dbt marks each of them SKIP. Models elsewhere in the DAG, with no path leading back to the failed test, keep building, so 1 bad column holds back only the part of the project that depends on it.

A failing test holds back everything to its right; the dashboard keeps reading the rows of the last build that passed.
  • raw.orders (source, raw table)
  • stg_orders (transform, dbt view)
  • unique_stg_orders_order_id (quality gate, dbt test)
  • order_totals (transform, dbt table)
  • payouts dashboard (consumer, reporting tool)
  • raw.orders -> stg_orders
  • stg_orders -> unique_stg_orders_order_id
  • unique_stg_orders_order_id -> order_totals
  • order_totals -> payouts dashboard
select
    order_id,
    customer_id,
    amount
from {{ source('raw', 'orders') }}
  1. dbt build
  2. dbt build
  3. dbt build
  • data_tests: unique, not_null
  • no data_tests
Order 103 arrives twice before the second build; with the unique test in place, order_totals is skipped and keeps its 3 correct rows.

In the first run, 4 raw rows build the stg_orders view, both tests on order_id pass, and order_totals builds 3 rows, with customer 1 at 2 orders and 100. The loader then delivers order 103 a second time. The second build creates stg_orders again and runs unique_stg_orders_order_id, which gets 1 row back and prints FAIL 1, so dbt skips order_totals and the table still holds the 3 rows from the first run. Once the duplicate is removed, the third build passes and rebuilds order_totals to the same 3 rows. Switch to the project without tests and the second build writes customer 1 as 3 orders and 160, which is the wrong figure a payouts report would have read.

LineNodePrinted
1 of 4stg_orders (view)OK
2 of 4not_null_stg_orders_order_idPASS
3 of 4unique_stg_orders_order_idFAIL 1
4 of 4order_totals (table)SKIP
SummaryDone.PASS=2 WARN=0 ERROR=1 SKIP=1 NO-OP=0 TOTAL=4

The summary counts a failed test under ERROR, which is also where a model whose SQL broke would land, so ERROR=1 on its own does not say which happened; the per-node lines do. SKIP is a status of its own: dbt sent no SQL for that node and wrote nothing, which leaves whatever the node held before. The build then exits with a non-zero code, and a later dbt build starts from the beginning, because dbt keeps no memory of which nodes were skipped.

Why the skip protects a table more than a view

order_totals is a table, so SKIP leaves the rows of the last good build in place and a dashboard that reads it keeps showing 100 for customer 1. Had order_totals been a view, SKIP would have left its definition in place too, but a view recomputes on every read from the current stg_orders, which now includes the duplicate, so anyone querying it would have seen 160 regardless. The skip protects stored rows; it cannot protect a view from its own upstream. That is a reason to materialize the models people read directly as tables when their inputs are tested.

Siblings carry on. Had the project also held a stg_customers model and a customer_count table built from it alone, both would build on the same run, because no edge connects them to the failed test. The build prints OK for them and SKIP for order_totals in the same log.

run then test, or build

dbt run, then dbt test
  • run builds stg_orders and order_totals in one pass
  • order_totals is written with customer 1 at 160 before any test runs
  • test then prints FAIL 1, after the wrong rows are already stored
  • the dashboard shows 160 until someone rebuilds
dbt build
  • stg_orders builds, then its tests run at once
  • unique returns 1 row and prints FAIL 1
  • order_totals is SKIP and keeps 100 for customer 1
  • the dashboard shows the last correct figure

The 2 routes send the same test query and print the same FAIL line. The difference is timing: dbt run finishes every model first, so a downstream table is already overwritten by the time dbt test reports the duplicate, and the test becomes a record of damage done. dbt build interleaves tests with models, so a failure arrives before the next model reads the bad rows.

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.