Catch bad data before the business sees it
Tests run on every build and fail loudly, so wrong records are caught at the source, not discovered in a board report.
equal_rowcount expected 1000 rows, got 1240 unique not_null accepted_values relationships severity: warn pipeline continues The issue is recorded and flagged for review, the run keeps going. Right for softer signals you want to watch but not block on.
severity: error pipeline stops The build fails on that branch. Bad data never reaches downstream tables or the dashboards the business reads.
A row count that should not have moved is caught at the source, not discovered in a board report.
Trust in your data is binary, and it is fragile. A leadership team can rely on the same dashboard for months, but the first time a number is visibly wrong, every other number on the page falls under suspicion too. The damage is rarely the single bad figure. It is the meeting that stops to argue about whether the data can be believed at all, and the slow drift back to private spreadsheets that follows.
The problem
Hand-rolled transformation pipelines almost never carry consistent quality rules. A source quietly sends nulls where it used to send IDs, a join fans out and doubles a row count, a feed stops refreshing over a long weekend. Nothing complains. The broken records flow straight through to the tables the business reads, and the failure is discovered in a board report rather than at the point it happened.
What dbt does about it
dbt makes correctness a property of every build. You declare expectations as tests right next to the models they protect. Generic tests cover the common rules out of the box: unique and not_null on keys, accepted_values for status columns, relationships to confirm every row ties back to its parent. When a rule is specific to your business, a singular test is just SQL that returns the rows that should not exist. Each test carries a severity, warn to watch or error to stop the build.
A common failsafe is a row-count check: when a join on a non-unique key quietly multiplies rows, a test that expected a thousand rows and finds more flags the problem at once. Because tests run interleaved with the models in dependency order, a failure with severity error halts that branch before the bad records reach anything downstream, while a warning simply records the issue and lets the run continue. You also declare your raw inputs as sources and attach freshness checks, so a feed that goes stale is caught as a failure, not noticed weeks later. The full set of options lives in the dbt data tests documentation.
What it looks like
A nightly run loads orders. An upstream change starts emitting duplicate order_id values. The unique test fails on the staging model, that branch stops, and the marts that feed the revenue dashboard simply do not rebuild on bad input. The on-call engineer gets a clear message naming the test and the offending rows. The board dashboard keeps showing yesterday’s trusted figure instead of today’s wrong one.
How we think about it
A test that only runs sometimes is not a guardrail, it is a hope. We make checks run on every build, keep them close to the data they describe, and tune severity so the gate is strict where it must be and forgiving where a warning is enough. Catching bad data is the first half of trust; proving the pipeline stays healthy over time is the other half, which is where reliability you can prove takes over.
Catch bad data before the business sees it, in short.
Does running tests on every build slow things down?
Tests run as lightweight SELECT queries beside your models, so the overhead is small and well worth it. You also choose where to spend the time: keep cheap checks like not_null and unique everywhere, and reserve heavier custom tests for the tables that matter most. A few seconds at build time is far cheaper than a wrong number in a board pack.
What is the difference between a test and an alert?
A test is a rule that decides whether data is correct, for example "order_id is never null and never duplicated". An alert is the message you get when a run fails. dbt is the test layer that produces the verdict, then your scheduler or CI raises the alert. See provable reliability for how those results turn into run history and notifications.
Can we add our own business rules as tests?
Yes. Beyond the built-in generic tests you write singular tests, ordinary SQL that returns the rows that should not exist, so any rule you can express in SQL becomes a test. Revenue is never negative, every invoice ties to a customer, a status is always one of an agreed set: all of these become checks that run on every build.
What is the difference between severity warn and error?
Severity error fails the build and stops bad data moving downstream, which is right for anything that would corrupt a trusted table. Severity warn records the problem and keeps going, which suits softer signals you want to watch but not block on. You set severity per test, so the gate is as strict as each rule deserves.
Keep exploring
A data catalog everyone trusts
dbt generates a searchable docs site with descriptions, owners, types and tests, built from the code that runs so it never drifts.
Data lineage you can follow
dbt maps every column from raw source to dashboard automatically, so you can trace where any number came from and see what a change will break before you make it.
Reliability you can prove
Every run leaves results, logs and freshness behind, and CI surfaces failures early, so you can show the pipeline is healthy.
Want this for your data?
If this is how you want your team to work, we should talk.