Building a robust data stack requires more than just clean SQL; it requires a rigorous validation framework that prevents broken dashboards from reaching stakeholders. In our work with mid-market data teams, we have found that implementing dbt testing best practices is the most effective way to ensure data integrity across the entire warehouse. By moving validation logic from ad hoc manual checks into the transformation layer itself, teams can catch regressions before they impact the business.
What are dbt testing best practices for modern data teams?
In the context of analytics engineering, dbt testing best practices refer to the systematic application of automated validation rules to SQL models to ensure accuracy, uniqueness, and consistency. We define a reliable pipeline as one where every transformation is bounded by tests that verify both the structure of the data and the business logic applied to it.
Our team categorizes these practices into three primary layers: foundational generic tests, complex singular tests, and package based statistical tests. Effective testing is not about writing as many tests as possible; it is about choosing the right test type for the specific failure mode you are trying to prevent.
The following table summarizes the primary testing types we deploy for our clients:
| Test Type | Implementation | Primary Purpose |
|---|---|---|
| Generic Tests | YAML declarations | Column level constraints: unique, not_null, relationship |
| Singular Tests | SQL in /tests directory |
Row level business logic and multi-table reconciliation |
| dbt_utils Tests | Package macros | Validating cross-column logic or accepted values at scale |
| dbt_expectations | Package macros | Statistical validation: mean, standard deviation, and data types |
Why should you prioritize dbt testing best practices early in your development cycle?
Waiting until a stakeholder reports a discrepancy in a Revenue dashboard is the most expensive way to discover a data quality issue. When we implement these workflows for scaling teams, we focus on the "shift left" principle: catching errors at the source or during the transformation process rather than at the visualization layer.
When pipelines lack a testing strategy, teams often face a phenomenon we call "silent failure." This occurs when a dbt run completes successfully, but the resulting data is logically incorrect due to duplicates in a source table or an unexpected null value in a join key. By adhering to dbt testing best practices, you convert these silent failures into loud, actionable alerts.
For teams looking to formalize these processes, our Data Foundation track provides hands-on training on setting up these environments using BigQuery and Terraform.
Implementing the four foundational generic tests
Every model in your dbt project should, at a minimum, include the four built in generic tests. These are configured directly in your schema.yml files and provide the highest ROI for the lowest effort.
1. Unique
The unique test ensures that a specific column (usually a primary key) contains no duplicate values. This is essential after performing joins that might inadvertently fan out your grain.
version: 2
models:
- name: stg_orders
columns:
- name: order_id
tests:
- unique2. Not_Null
This test validates that every row in a column contains a value. We use this on join keys and mandatory fields like created_at or customer_id.
3. Relationships
The relationships test is dbt’s version of a foreign key constraint. It ensures that every value in a column exists in a related table. For example, every order_id in your fct_order_items table must exist in the dim_orders table.
4. Accepted_Values
Use this to restrict a column to a specific set of strings. This is particularly useful for status columns such as ['pending', 'shipped', 'delivered', 'cancelled'].
Advanced testing with singular SQL tests
Generic tests are powerful but limited to single columns or simple relationships. When you need to validate complex business logic, you must use singular tests. These are standalone .sql files stored in your /tests directory.
A singular test is simply a SELECT statement that returns the rows that fail the test. If the query returns zero rows, the test passes. If it returns one or more rows, the test fails.
For example, if you want to ensure that the total discount amount never exceeds the total order amount, you would write a singular test:
-- tests/assert_discount_less_than_total.sql
select
order_id,
total_amount,
discount_amount
from {{ ref('fct_orders') }}
where discount_amount > total_amountIn our experience, singular tests are the best place to encode "tribal knowledge" about your business. If your operations lead tells you that a "Refund" can never happen before an "Order Date," turn that into a singular test immediately. This prevents the same logical error from recurring as your codebase grows.
Ready to fix your data foundation?
Book a free diagnostic call and find out where your stack stands.
Book a CallLeveraging packages for statistical data quality
As your data foundation matures, basic constraints are often insufficient. You may need to verify that a column’s distribution remains consistent or that a date column does not have gaps. This is where packages like dbt_utils and dbt_expectations become mandatory.
The dbt_expectations package is a port of Great Expectations into dbt. It allows you to perform sophisticated checks such as:
- Verifying that a column is of a specific data type.
- Ensuring a numeric value falls within a specific range of standard deviations from the mean.
- Checking that the proportion of nulls in a column does not exceed a certain threshold.
If you are unsure whether your current stack can support these advanced validations, our AI Stack Audit provides a technical assessment of your data foundation and identifies gaps in your testing coverage.
How to structure tests for BigQuery and Snowflake performance
Running hundreds of tests on every dbt run can become expensive and slow if not managed correctly. We recommend a tiered approach to test execution.
Tier 1: Development Testing
During local development, use the --select flag to run tests only on the models you are actively changing. This saves compute costs and provides faster feedback loops.
Tier 2: CI/CD Testing
When a developer submits a Pull Request (PR), your CI/CD pipeline should run tests on the modified models and their immediate downstream dependents. This ensures that a change in a staging model does not break a core fact table.
Tier 3: Production Testing
In production, run the full test suite. We often use the store_failures configuration for production runs. This creates a table in a dedicated audit schema containing the specific rows that caused a test to fail, making debugging significantly faster for your data engineers.
dbt test --store-failuresManaging test failures with severity levels
Not all test failures are created equal. A primary key violation in a financial table is a "stop the world" event, whereas an unexpected string in a marketing source might just be a warning.
dbt allows you to configure severity levels:
- Error: The dbt run will stop (if using
dbt build) or exit with an error code. This is for critical data integrity checks. - Warn: The test will record a failure, but the pipeline will continue. This is ideal for monitoring data health without breaking downstream dependencies.
We suggest a strict "Error" policy for all unique and not_null tests on primary keys. Use "Warn" for experimental data sources or non-critical descriptive fields.
Data contracts and the future of dbt testing
One of the more recent additions to the dbt ecosystem is the concept of Data Contracts. While traditional testing validates data after it has been produced, Data Contracts allow you to define the expected schema and constraints at the model definition level.
If a model is configured with a contract, dbt will verify that the SQL produces the correct column names and types before it even attempts to run the transformation. This is a powerful extension of dbt testing best practices because it catches schema drift before it enters your production environment.
For teams building production AI agents, these contracts are vital. An AI agent relying on a specific column type will fail if that type changes unexpectedly. We cover these production grade requirements in our AI Builders track.
Frequently Asked Questions About dbt testing best practices
How many tests should a dbt model have?
There is no fixed number, but every model should have at least a unique and not_null test on its primary key. We recommend adding relationships tests for all foreign keys and singular tests for any column involving complex business logic or multi-table aggregations.
What is the difference between dbt test and dbt build?
dbt test only runs the validation queries against existing tables. dbt build is a more comprehensive command that runs models, tests, snapshots, and seeds in the correct dependency order. If a test fails during dbt build, downstream models will not be created, preventing the propagation of bad data.
Should I test my staging models or only my final marts?
You should test both. Testing staging models helps you catch source data issues as early as possible. Testing marts ensures that your transformations (joins, filters, and aggregations) have not introduced logical errors. A "source to mart" testing strategy is the most reliable way to maintain data quality.
How do I handle tests that fail due to known issues in source data?
For known issues that cannot be fixed immediately at the source, you can use the where configuration in your tests to exclude certain records. Alternatively, you can set the severity to warn and use a config block to set a warn_if threshold that allows a certain number of failures before triggering an alert.
Can I use dbt tests to validate data across different databases?
Standard dbt tests are designed to run within the same database or warehouse. If you need to validate data across different systems, such as comparing BigQuery data to a PostgreSQL source, you would typically use an external data quality tool or a custom Python based validation script within your orchestration layer.
Ready to build a more reliable data foundation?
If your team is struggling with broken pipelines or inconsistent metrics, we can help you implement a production grade testing framework. Our team works with scaling data organizations to audit their existing stacks and deploy automated validation systems that build trust with stakeholders.
Whether you need a full AI Stack Audit to identify architectural gaps or want to upskill your team through our Data Engineering Bootcamp, we provide the practitioner led expertise required to move beyond fragile SQL and into a reliable, tested data ecosystem. Book a free consultation to discuss your data quality roadmap.