dbt Singular Tests

Singular tests are custom SQL files that check complex data quality rules that the four built-in generic tests cannot express. You write a SELECT statement that returns failing rows. If the query returns any rows, the test fails. If it returns zero rows, the test passes. Singular tests live in the tests/ folder of your project.

When to Use Singular Tests

Use singular tests for business rules that involve multiple columns, multiple tables, or mathematical constraints. Generic tests cover simple column-level checks. Singular tests cover everything else.

Generic tests handle:              Singular tests handle:
------------------------           ---------------------------
Column is not null                 Revenue must be positive
Column values are unique           Refund amount cannot exceed order amount
Values match an allowed list       Each order must have at least one line item
Foreign key exists                 Shipping date must be after order date

Writing Your First Singular Test

Create a SQL file inside the tests/ folder. The file name becomes the test name. Write a SELECT that returns only the rows that fail the check:

-- tests/assert_order_amount_positive.sql

-- This test fails if any order has a zero or negative amount
select
    order_id,
    amount_dollars
from {{ ref('stg_orders') }}
where amount_dollars <= 0

Running dbt test executes this SQL. If any orders have zero or negative amounts, those rows come back and dbt reports a test failure with the row count.

More Singular Test Examples

Test: Refund Cannot Exceed Order Total

-- tests/assert_refund_lte_order_amount.sql

select
    r.refund_id,
    r.order_id,
    r.refund_amount,
    o.amount_dollars
from {{ ref('stg_refunds') }} r
join {{ ref('stg_orders') }}  o on r.order_id = o.order_id
where r.refund_amount > o.amount_dollars

Test: Shipping Date After Order Date

-- tests/assert_ship_date_after_order_date.sql

select
    order_id,
    order_date,
    ship_date
from {{ ref('stg_orders') }}
where ship_date < order_date
  and ship_date is not null

Test: Every Order Has At Least One Line Item

-- tests/assert_order_has_line_items.sql

select
    o.order_id
from {{ ref('stg_orders') }} o
left join {{ ref('stg_order_items') }} li
  on o.order_id = li.order_id
where li.order_id is null

Test: Revenue Matches Sum of Line Items

-- tests/assert_order_total_matches_line_items.sql

select
    o.order_id,
    o.amount_dollars                         as header_total,
    sum(li.quantity * li.unit_price)         as calculated_total,
    abs(o.amount_dollars - sum(li.quantity * li.unit_price)) as discrepancy
from {{ ref('stg_orders') }} o
join {{ ref('stg_order_items') }} li on o.order_id = li.order_id
group by o.order_id, o.amount_dollars
having abs(o.amount_dollars - sum(li.quantity * li.unit_price)) > 0.01

Running Singular Tests

# Run all tests including singular tests
dbt test

# Run only singular tests
dbt test --select test_type:singular

# Run a specific singular test by file name
dbt test --select assert_order_amount_positive

Adding Configuration to Singular Tests

You can add a config block at the top of a singular test file to control severity, tags, and other settings:

-- tests/assert_refund_lte_order_amount.sql
{{ config(severity='warn', tags=['finance']) }}

select
    r.refund_id,
    r.refund_amount,
    o.amount_dollars
from {{ ref('stg_refunds') }} r
join {{ ref('stg_orders') }}  o on r.order_id = o.order_id
where r.refund_amount > o.amount_dollars

Singular Tests vs Generic Tests: Decision Guide

Scenario                                    Test Type
----------------------------------------    ----------
Column must not be null                     Generic (not_null)
Column values must be unique                Generic (unique)
Column values must match a list             Generic (accepted_values)
Foreign key must exist in another table     Generic (relationships)
Two columns have an invalid relationship    Singular
Aggregated value must match another         Singular
Cross-table business rule                   Singular
Time-sequence constraint                    Singular

Organizing Singular Tests

As your project grows, organize singular test files into subfolders:

tests/
  finance/
    assert_refund_lte_order_amount.sql
    assert_revenue_is_positive.sql
  fulfillment/
    assert_ship_date_after_order_date.sql
    assert_order_has_line_items.sql
  integrity/
    assert_order_total_matches_line_items.sql

Tag tests by category so you can run them in groups:

{{ config(tags=['finance']) }}
# Run all finance tests
dbt test --select tag:finance

Debugging a Failing Singular Test

When a singular test fails, dbt tells you how many rows failed. Run the test SQL directly in your warehouse to see the actual failing rows:

-- Run the test SQL manually in your warehouse:
select
    order_id,
    amount_dollars
from dbt_dev.stg_orders
where amount_dollars <= 0

-- Returns:
order_id | amount_dollars
---------|---------------
1047     | 0.00
2198     | -5.99

Now you see exactly which orders have invalid amounts and can trace the problem to its source.

Leave a Comment

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