Testing in dbt: Test Generici Built-in, Test SQL Personalizzati e dbt-expectations
dbt test garantisce la qualità dei dati: 4 test generici built-in (unique, not_null, accepted_values, relationships), test SQL singolari per logica di business, e dbt-expectations per asserzioni stile Great Expectations - tutto eseguito in CI/CD.
Perché il Testing dei Dati è Non Negoziabile
Un data warehouse senza test è una fonte di dati che potrebbe essere corretta.
I modelli dbt trasformano dati grezzi imperfetti: una colonna order_id duplicata
nella sorgente, un valore NULL inaspettato in una chiave di join, un country code
fuori dall'enum atteso - questi problemi entrano silenziosi nel warehouse e producono
dashboard errati che nessuno scopre finché non arriva una riunione imbarazzante.
dbt integra un sistema di testing nativo che rende i controlli di qualità una parte
dichiarativa del progetto, non un afterthought. I test si eseguono con dbt test,
si scrivono in YAML accanto ai modelli, e falliscono il CI prima che i dati corrotti raggiungano
la produzione.
I 4 Test Generici Built-in
dbt Core include quattro test generici pronti all'uso, configurabili tramite YAML nel file
schema.yml del modello:
# models/staging/schema.yml
version: 2
models:
- name: stg_orders
description: "Ordini grezzi dalla sorgente transazionale, normalizzati."
columns:
- name: order_id
description: "Identificatore univoco dell'ordine."
tests:
- unique # nessun duplicato
- not_null # nessun NULL
- name: customer_id
description: "Chiave esterna verso stg_customers."
tests:
- not_null
- relationships:
to: ref('stg_customers')
field: customer_id
- name: status
description: "Stato corrente dell'ordine."
tests:
- not_null
- accepted_values:
values: ['pending', 'processing', 'completed', 'cancelled', 'refunded']
- name: total_amount
description: "Importo totale lordo in EUR."
tests:
- not_null
Dettaglio dei 4 Test
unique
Verifica che nessun valore si ripeta nella colonna. dbt esegue una query
GROUP BY ... HAVING COUNT(*) > 1 - il test fallisce se il risultato
ha almeno una riga. Cruciale per primary key e natural key.
tests:
- unique
# unique su chiave composta (dbt 1.6+)
- dbt_utils.unique_combination_of_columns:
combination_of_columns:
- order_id
- line_item_id
not_null
Verifica l'assenza di valori NULL. Il test fallisce se COUNT(col) != COUNT(*).
Applica a tutte le chiavi di join e colonne obbligatorie nel contratto dati.
tests:
- not_null
# Con where per escludere casi noti
- not_null:
where: "status != 'draft'"
config:
severity: warn # warn invece di error
accepted_values
Verifica che tutti i valori rientrino in un insieme predefinito. Utile per colonne di tipo categorico (status, country, tier).
tests:
- accepted_values:
values:
- 'completed'
- 'pending'
- 'cancelled'
quote: true # aggiunge virgolette (default true per stringhe)
config:
severity: warn
relationships
Verifica l'integrità referenziale: ogni valore della colonna deve esistere nel modello target. L'equivalente di una FOREIGN KEY.
tests:
- relationships:
to: ref('stg_customers')
field: customer_id
# Gestisce NULL: i NULL non falliscono il test
config:
severity: error
Configurare Severità e Threshold
Non tutti i test devono bloccare la pipeline. dbt permette di configurare la severità e una soglia di errore accettabile:
# models/marts/schema.yml
models:
- name: fct_orders
columns:
- name: discount_pct
tests:
- not_null:
config:
severity: warn # warn: il test fallisce ma dbt non esce con codice errore
- name: customer_id
tests:
- relationships:
to: ref('dim_customers')
field: customer_id
config:
severity: error # error: blocca la pipeline (default)
error_if: ">10" # fallisce solo se >10 righe violano il vincolo
warn_if: ">0" # warn se anche una sola riga viola il vincolo
- name: order_id
tests:
- unique:
config:
severity: error
store_failures: true # salva le righe che falliscono in una tabella
schema: test_failures # schema dedicato per le failure
Test SQL Singolari: Logica di Business Personalizzata
I test generici coprono i controlli strutturali. Per la logica di business più complessa,
dbt supporta i test singolari: file SQL nella directory tests/
che restituiscono righe di errore. Se la query restituisce 0 righe, il test è passato.
-- tests/assert_orders_total_positive.sql
-- Un test singolare: ogni query che restituisce righe causa il fallimento del test.
-- La convenzione è: scrivi la query che trova i casi ANOMALI.
SELECT
order_id,
total_amount,
'total_amount deve essere positivo' AS failure_reason
FROM {{ ref('fct_orders') }}
WHERE total_amount <= 0
-- tests/assert_revenue_matches_line_items.sql
-- Verifica la coerenza tra header ordine e dettaglio righe
WITH order_totals AS (
SELECT
order_id,
SUM(line_amount) AS calculated_total
FROM {{ ref('fct_order_line_items') }}
GROUP BY order_id
),
orders AS (
SELECT order_id, total_amount
FROM {{ ref('fct_orders') }}
)
SELECT
o.order_id,
o.total_amount AS header_total,
ot.calculated_total AS calculated_total,
ABS(o.total_amount - ot.calculated_total) AS discrepancy
FROM orders o
JOIN order_totals ot ON o.order_id = ot.order_id
WHERE ABS(o.total_amount - ot.calculated_total) > 0.01 -- tolleranza 1 centesimo
-- tests/assert_no_future_orders.sql
-- Nessun ordine può avere data nel futuro (problema comune in migrazioni dati)
SELECT
order_id,
created_at,
CURRENT_TIMESTAMP AS test_run_time
FROM {{ ref('stg_orders') }}
WHERE created_at > CURRENT_TIMESTAMP
Test Singolari con Parametri
-- tests/assert_metric_freshness.sql
-- Verifica che i dati non siano più vecchi di N ore (configurabile)
SELECT
MAX(event_timestamp) AS last_event,
CURRENT_TIMESTAMP AS now,
DATEDIFF('hour', MAX(event_timestamp), CURRENT_TIMESTAMP) AS hours_lag
FROM {{ ref('fct_events_hourly') }}
HAVING DATEDIFF('hour', MAX(event_timestamp), CURRENT_TIMESTAMP) > {{ var('max_lag_hours', 4) }}
-- Esecuzione con override della variabile:
-- dbt test --select assert_metric_freshness --vars '{"max_lag_hours": 2}'
Test Generici Personalizzati (Custom Generic Tests)
Quando lo stesso controllo va applicato a più modelli, puoi creare test generici
personalizzati nella directory tests/generic/. Sono macro Jinja che accettano
il nome del modello e della colonna come parametri.
-- tests/generic/is_positive.sql
-- Test generico riutilizzabile: verifica che una colonna sia > 0
{% test is_positive(model, column_name) %}
SELECT {{ column_name }}
FROM {{ model }}
WHERE {{ column_name }} IS NOT NULL
AND {{ column_name }} <= 0
{% endtest %}
-- tests/generic/is_between.sql
-- Test generico con parametri aggiuntivi
{% test is_between(model, column_name, min_value, max_value) %}
SELECT
{{ column_name }},
{{ min_value }} AS expected_min,
{{ max_value }} AS expected_max
FROM {{ model }}
WHERE {{ column_name }} < {{ min_value }}
OR {{ column_name }} > {{ max_value }}
{% endtest %}
# Utilizzo nel schema.yml
models:
- name: fct_orders
columns:
- name: total_amount
tests:
- is_positive # usa il test generico custom
- name: discount_pct
tests:
- is_between:
min_value: 0
max_value: 1
dbt-expectations: Asserzioni Stile Great Expectations
Il package dbt-expectations porta nel mondo dbt l'approccio di Great Expectations: un catalogo ricco di test statistici e di distribuzione che vanno oltre i semplici controlli strutturali.
# packages.yml
packages:
- package: calogica/dbt_expectations
version: 0.10.4
# dbt deps per installare
# models/marts/schema.yml - dbt-expectations in azione
models:
- name: fct_orders
tests:
# Numero di righe atteso nel range [1000, 1000000]
- dbt_expectations.expect_table_row_count_to_be_between:
min_value: 1000
max_value: 1000000
# La colonna non deve avere troppi NULL
- dbt_expectations.expect_column_values_to_not_be_null:
column_name: order_id
columns:
- name: total_amount
tests:
# Tutti i valori devono essere >= 0
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 99999
# Distribuzione: media attesa tra 50 e 500 EUR
- dbt_expectations.expect_column_mean_to_be_between:
min_value: 50
max_value: 500
# Nessun outlier estremo (z-score)
- dbt_expectations.expect_column_stdev_to_be_between:
min_value: 0
max_value: 1000
- name: order_date
tests:
# Le date devono essere nel passato
- dbt_expectations.expect_column_values_to_be_between:
min_value: "'2020-01-01'::date"
max_value: "current_date"
row_condition: "status = 'completed'"
- name: email
tests:
# Formato email valido con regex
- dbt_expectations.expect_column_values_to_match_regex:
regex: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
- name: status
tests:
# Distribuzione dei valori: completed deve essere > 60% degli ordini
- dbt_expectations.expect_column_proportion_of_unique_values_to_be_between:
min_value: 0.01 # ogni status deve esistere
max_value: 0.99
Test di Freshness sui Modelli
# Verifica freshness dei dati direttamente nel test
models:
- name: fct_events_hourly
tests:
# L'evento più recente deve essere meno di 2 ore fa
- dbt_expectations.expect_row_values_to_have_recent_data:
column_name: event_timestamp
datepart: hour
interval: 2
Organizzare i Test nel Progetto
Un progetto dbt ben organizzato distribuisce i test su tutti i layer, con densità crescente man mano che ci si avvicina ai modelli esposti ai consumer:
# Struttura consigliata dei test per layer
#
# staging/ - test strutturali di base
# unique + not_null sulle primary key
# relationships verso le sorgenti
# accepted_values sulle colonne categoriche
#
# intermediate/ - test di consistenza
# Nessun test se i modelli sono ephemeral
# Test critici se sono materializzati
#
# marts/ - test completi + business logic
# Tutti i test di staging
# Test singolari per le regole di business
# dbt-expectations per distribuzione e range
# store_failures=true per debug in produzione
# Esecuzione selettiva dei test:
# dbt test # tutti i test
# dbt test --select stg_orders # test su un modello
# dbt test --select tag:critical # test con tag specifico
# dbt test --select source:raw # test sulle sorgenti
# dbt build # run + test insieme (raccomandato in CI)
dbt build vs dbt run + dbt test
In CI/CD, preferisci sempre dbt build a dbt run && dbt test.
dbt build esegue i test dopo ogni nodo del DAG, non solo alla fine:
se un modello intermedio fallisce un test critico, i modelli downstream non vengono
neanche eseguiti, risparmiando tempo e compute.
Documentare i Test nel Catalogo dbt
I test definiti in YAML appaiono automaticamente nel catalogo generato da dbt docs generate.
Aggiungere descrizioni ai test migliora la comprensione per il team:
models:
- name: stg_orders
columns:
- name: status
description: >
Stato del ciclo di vita dell'ordine. I valori ammessi riflettono
il workflow: pending (creato) → processing (pagato) → completed
(spedito+consegnato), oppure cancelled o refunded in qualsiasi fase.
tests:
- not_null:
config:
description: "Il campo status non può mai essere NULL."
- accepted_values:
values: ['pending', 'processing', 'completed', 'cancelled', 'refunded']
config:
description: "Valori al di fuori di questo insieme indicano bug nel sistema sorgente."
Eseguire i Test: Comandi e Opzioni Utili
dbt test è flessibile: puoi eseguire tutti i test, selezionarne un sottoinsieme per modello, per tipo o per tag. Conoscere questi comandi accelera il ciclo di sviluppo:
-- Esegui tutti i test del progetto
dbt test
-- Test su un modello specifico
dbt test --select stg_orders
-- Test su tutti i modelli che dipendono da stg_orders (downstream)
dbt test --select stg_orders+
-- Test solo dei modelli con un tag specifico
dbt test --select tag:critical
-- Test solo su sorgenti (freshness + test dichiarati in sources.yml)
dbt test --select source:raw
-- Escludere modelli lenti in sviluppo
dbt test --exclude tag:slow
-- Eseguire test specifici per tipo (generic o singular)
dbt test --select test_type:generic
dbt test --select test_type:singular
-- dbt build: run + test insieme (consigliato in CI/CD)
-- Ogni nodo del DAG viene testato prima di procedere al nodo successivo
dbt build --select tag:daily
Debug dei Test Falliti con store_failures
# Configurazione globale: salva le righe che falliscono in tabelle dedicate
# dbt_project.yml
tests:
+store_failures: true # abilita per tutti i test
+store_failures_as: table # 'table' o 'view'
+schema: test_failures # schema dedicato per le tabelle di failure
# Oppure per singolo test:
models:
- name: fct_orders
columns:
- name: customer_id
tests:
- relationships:
to: ref('dim_customers')
field: customer_id
config:
store_failures: true
schema: test_failures
-- Dopo dbt test, interroga la tabella delle failure:
-- Schema: test_failures (o quello configurato)
-- Tabella: relationships_fct_orders_customer_id__customer_id__ref_dim_customers_ (nome auto-generato)
SELECT
customer_id,
COUNT(*) AS occurrences
FROM test_failures.relationships_fct_orders_customer_id__customer_id__ref_dim_customers_
GROUP BY 1
ORDER BY 2 DESC
LIMIT 20;
-- Questo ti mostra esattamente quali customer_id non esistono in dim_customers --
-- utile per capire se è un problema di dati o di logica del modello.
Integrare i Test nel Workflow di Sviluppo
I test dbt sono più utili quando fanno parte del ciclo di sviluppo normale, non solo del CI. Ecco il workflow consigliato quando sviluppi o modifichi un modello:
Workflow Test-First per dbt
- Scrivi prima i test: prima di implementare il modello, definisci in schema.yml i test che deve superare (unique, not_null, relationships, accepted_values). Questo funge da specifica del contratto dati.
-
Implementa il modello: scrivi il SQL e verifica che i test passino
localmente con
dbt build --select my_model. - Aggiungi test di business: scrivi test singolari per le regole di business specifiche del modello (importi positivi, date coerenti, proporzioni attese).
- dbt-expectations per la distribuzione: aggiungi almeno un test sulla distribuzione dei valori critici (media, range) per rilevare anomalie future.
- PR con CI: il workflow GitHub Actions verifica che tutti i test passino prima del merge.
Conclusioni e Prossimi Passi
Il sistema di testing dbt trasforma la qualità dei dati da processo manuale a garanzia automatizzata. I test generici built-in coprono i controlli strutturali fondamentali, i test singolari catturano le regole di business, dbt-expectations porta rigor statistico.
La chiave è non trattare i test come un'aggiunta opzionale: scrivili insieme ai modelli,
eseguili in CI su ogni PR, e usa store_failures=true in produzione per debug rapido.
Il prossimo modulo completa il quadro della qualità dei dati: la documentazione automatica di dbt. Come il sistema di descrizioni YAML, il lineage graph e il catalogo interattivo rendono il warehouse comprensibile a tutto il team.

Commenti
Caricamento commenti...
Accedi per commentare