dbt Custom Generic Tests

dbt ships with four built-in generic tests: not_null, unique, accepted_values, and relationships. Custom generic tests let you write additional reusable tests that work exactly like the built-ins — declare them in YAML with arguments, and dbt runs the generated SQL automatically. Write a custom generic test once and apply it to any column in any model.

Generic Test vs Singular Test

Singular test:    one SQL file, tests one specific thing in one model
Generic test:     one reusable template, applied to many models via YAML

Use singular when: the check is one-off and specific to one model
Use generic when:  the same check pattern applies across multiple models

Creating a Custom Generic Test

Custom generic tests live in the macros/ folder (or any tests/generic/ subfolder). They use a Jinja macro named test_<name>. The macro must return a SQL SELECT that returns failing rows:

Example: test_is_positive

-- macros/test_is_positive.sql

{% test is_positive(model, column_name) %}

select
    {{ column_name }}
from {{ model }}
where {{ column_name }} is not null
  and {{ column_name }} <= 0

{% endtest %}

The two required parameters are always model (the table being tested) and column_name (the column to check). dbt injects these automatically when you apply the test in YAML.

Using the Custom Test in YAML

# models/marts/schema.yml
models:
  - name: fct_orders
    columns:
      - name: amount_dollars
        tests:
          - not_null
          - is_positive        ← your custom generic test

      - name: quantity
        tests:
          - not_null
          - is_positive        ← same test reused on a different column

Custom Generic Test with Additional Arguments

Add extra parameters to make the test flexible:

-- macros/test_is_between.sql

{% test is_between(model, column_name, min_value, max_value) %}

select
    {{ column_name }}
from {{ model }}
where {{ column_name }} is not null
  and {{ column_name }} not between {{ min_value }} and {{ max_value }}

{% endtest %}
# Usage in YAML
columns:
  - name: discount_percent
    tests:
      - is_between:
          min_value: 0
          max_value: 100

  - name: rating
    tests:
      - is_between:
          min_value: 1
          max_value: 5

Custom Test: No Future Dates

-- macros/test_not_in_future.sql

{% test not_in_future(model, column_name) %}

select {{ column_name }}
from {{ model }}
where {{ column_name }} > current_date

{% endtest %}
# Usage
columns:
  - name: order_date
    tests:
      - not_null
      - not_in_future

  - name: ship_date
    tests:
      - not_in_future

Custom Test: Ratio Within Range

-- macros/test_proportion_above_threshold.sql

{% test proportion_above_threshold(model, column_name, threshold) %}

with counts as (
    select
        count(*)                                        as total_rows,
        count(case when {{ column_name }} is not null
                   then 1 end)                         as non_null_rows
    from {{ model }}
)

select total_rows
from counts
where non_null_rows::float / nullif(total_rows, 0) < {{ threshold }}

{% endtest %}
# Usage: email must be non-null for at least 80% of rows
columns:
  - name: email
    tests:
      - proportion_above_threshold:
          threshold: 0.80

Custom Test: Column Matches Another Column

-- macros/test_column_gte_other_column.sql

{% test column_gte_other_column(model, column_name, other_column) %}

select {{ column_name }}, {{ other_column }}
from {{ model }}
where {{ column_name }} < {{ other_column }}

{% endtest %}
# Usage: ship_date must be >= order_date
columns:
  - name: ship_date
    tests:
      - column_gte_other_column:
          other_column: order_date

Adding Config to a Custom Test

You can set default severity and other config inside the test macro:

{% test is_positive(model, column_name) %}
{{ config(severity='warn') }}

select {{ column_name }}
from {{ model }}
where {{ column_name }} is not null
  and {{ column_name }} <= 0

{% endtest %}

Override the config at the YAML level when applying the test:

tests:
  - is_positive:
      config:
        severity: error
        tags: ['finance']

Organising Custom Tests

macros/
  tests/
    test_is_positive.sql
    test_is_between.sql
    test_not_in_future.sql
    test_proportion_above_threshold.sql
    test_column_gte_other_column.sql

Storing test macros in a tests/ subfolder inside macros/ keeps them separate from transformation macros and makes them easy to find.

Sharing Custom Tests via Packages

If multiple dbt projects in your organisation need the same custom tests, publish them as an internal dbt package. Teams install the package with dbt deps and apply the tests in YAML immediately — the same way they use dbt_utils or dbt_expectations.

Leave a Comment

Your email address will not be published. Required fields are marked *