Salta al contenuto principale

05 - Testing in dbt: Test Generici Built-in, Test SQL Personalizzati e dbt-expectations

dbt 14 min Testing in dbt: Test Generici Built-in, Test SQL Personalizzati e dbt-expectations dbt test garantisce la qualità dei dati: 4 test generici...

Cosa ne pensi di questo articolo?

In Breve

dbt 14 min Testing in dbt: Test Generici Built-in, Test SQL Personalizzati e dbt-expectations dbt test garantisce la qualità dei dati: 4 test generici...

dbt 14 min

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.

dbt test dbt generic tests unique not_null dbt dbt-expectations data quality dbt

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

  1. 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.
  2. Implementa il modello: scrivi il SQL e verifica che i test passino localmente con dbt build --select my_model.
  3. Aggiungi test di business: scrivi test singolari per le regole di business specifiche del modello (importi positivi, date coerenti, proporzioni attese).
  4. dbt-expectations per la distribuzione: aggiungi almeno un test sulla distribuzione dei valori critici (media, range) per rilevare anomalie future.
  5. 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.

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti