Adrienne Vermorel

dbt-expectations : le package indispensable pour tout projet

Nous avons passé les trois derniers articles à apprendre comment tester unitairement vos modèles dbt. Vous savez maintenant mocker des inputs, valider la logique de transformation et détecter les bugs avant qu’ils n’atteignent la production. Les tests unitaires vérifient que votre code fonctionne correctement. Ils ne vérifient pas que vos données sont saines.

Votre SQL peut être parfait et produire quand même des résultats aberrants. Un système source envoie des nulls là où il n’y en avait jamais. Un batch quotidien arrive avec six heures de retard. Un changement en amont fait varier votre panier moyen de 50%. Vos tests unitaires passent. Vos dashboards cassent.

C’est là qu’intervient dbt-expectations. C’est un package de plus de 50 tests de qualité de données pré-construits qui détectent les problèmes qu’un SQL correct ne peut pas prévenir. Là où les tests unitaires demandent “ma logique de transformation fonctionne-t-elle ?”, dbt-expectations demande “mes données de production sont-elles saines ?”

Combiné aux compétences de tests unitaires des articles 1-3, dbt-expectations complète votre stratégie de tests.

Ce que les tests natifs de dbt ne peuvent pas faire

Par défaut, dbt propose quatre tests génériques : unique, not_null, accepted_values et relationships. Ils couvrent les bases, mais laissent des lacunes significatives.

Pouvez-vous valider qu’une colonne email contient des emails correctement formatés ? Non. Pouvez-vous vérifier qu’une colonne timestamp contient des données récentes ? Uniquement pour les sources, pas pour les modèles. Pouvez-vous vérifier qu’une clé composite sur plusieurs colonnes est unique ? Non. Pouvez-vous détecter quand la valeur moyenne d’une métrique sort de la plage normale ? Non.

dbt-expectations comble chacune de ces lacunes. Il apporte la validation de patterns, la validation statistique, les contrôles de fraîcheur sur n’importe quel modèle, les tests multi-colonnes et les tests conditionnels à votre projet dbt, le tout sans quitter SQL.

Installation et configuration

Ajoutez le package à votre packages.yml :

packages:
- package: metaplane/dbt_expectations
version: [">=0.10.0", "<0.11.0"]

Le package nécessite une variable de fuseau horaire pour les tests basés sur les dates. Ajoutez ceci à votre dbt_project.yml :

vars:
'dbt_date:time_zone': 'Europe/Paris'

Exécutez dbt deps et vous êtes prêt. Le package récupère automatiquement dbt-date et dbt-utils comme dépendances, vous n’avez donc pas à les gérer séparément.

dbt-expectations nécessite dbt 1.8. Il supporte entièrement BigQuery, Snowflake, Postgres, Redshift, DuckDB et Trino.

Tests unitaires vs tests de données : le tableau complet

Avant d’examiner des tests spécifiques, voici comment dbt-expectations s’intègre aux tests unitaires que vous avez appris dans les articles 1-3.

AspectTests unitaires (dbt 1.8+)dbt-expectations
Ce qui est testéLa logique de transformationLa qualité des données
Données en entréeDes fixtures mockées que vous définissezLes données de production réelles
Quand il s’exécutePipeline CI sur les changements de codeChaque exécution de dbt build/test
Ce qu’il détecteBugs logiques, cas limitesAnomalies de données, problèmes de sources
Question exemple”Mon CASE WHEN catégorise-t-il correctement ?""Toutes les valeurs sont-elles dans la plage attendue ?”

Voyez cela comme deux points de contrôle dans votre pipeline de données :

Changements de code → Tests unitaires → Déploiement → Tests de données → Dashboard

Les tests unitaires protègent vos déploiements. Les tests de données protègent vos données. Vous avez besoin des deux.

Un exemple concret : vous avez un modèle qui calcule le revenu en multipliant la quantité par le prix unitaire. Votre test unitaire vérifie que 3 * 10.00 = 30.00 (la logique de multiplication fonctionne). Votre test dbt-expectations vérifie que les valeurs de revenu résultantes en production sont comprises entre 0 et 10 000 000 (les données sont cohérentes). Le premier détecte un bug si quelqu’un modifie la formule. Le second détecte un problème si un système source envoie soudainement des quantités négatives.

Les tests qui comptent le plus

Avec plus de 50 tests disponibles, vous n’avez pas besoin de tous les apprendre. Voici les tests à plus forte valeur ajoutée dans chaque catégorie, avec des exemples spécifiques à BigQuery.

Tests au niveau de la table

expect_row_values_to_have_recent_data

C’est probablement le test le plus précieux de tout le package. Le dbt natif n’offre des contrôles de fraîcheur que sur les sources. Ce test fonctionne sur n’importe quel modèle, détectant les données obsolètes avant que vos dashboards n’affichent les chiffres d’hier comme ceux d’aujourd’hui.

models:
- name: mrt__sales__orders
columns:
- name: order_timestamp
tests:
- dbt_expectations.expect_row_values_to_have_recent_data:
datepart: hour
interval: 24

Ce test échoue si aucune ligne n’a un order_timestamp dans les dernières 24 heures. Pour les données GA4, qui ont généralement un délai de 24-48 heures, vous mettriez interval: 48.

expect_table_row_count_to_equal_other_table

Les transformations ne devraient pas perdre des lignes silencieusement. Ce test le détecte quand ça arrive :

models:
- name: mrt__sales__orders
tests:
- dbt_expectations.expect_table_row_count_to_equal_other_table:
compare_model: ref('base__shopify__orders')

Si votre modèle de base a 50 000 lignes et votre mart en a 49 000, quelque chose s’est mal passé. Ce test vous le dit immédiatement.

expect_table_row_count_to_be_between

Détectez les changements de volume inattendus. Si votre batch quotidien contient normalement 10 000 à 100 000 lignes et en a soudainement 500, vous voulez le savoir :

models:
- name: base__ga4__events
tests:
- dbt_expectations.expect_table_row_count_to_be_between:
min_value: 10000
max_value: 100000

Validation de patterns

expect_column_values_to_match_regex

Le dbt natif n’a rien pour la validation de format. Ce test comble cette lacune :

columns:
- name: customer_email
tests:
- dbt_expectations.expect_column_values_to_match_regex:
regex: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'

expect_column_values_to_match_like_pattern

Quand les regex sont excessives, utilisez plutôt les patterns SQL LIKE :

columns:
- name: product_sku
tests:
- dbt_expectations.expect_column_values_to_match_like_pattern:
like_pattern: 'PRD-%'

Validation des plages de valeurs

expect_column_values_to_be_between

Détectez les valeurs impossibles avant qu’elles ne corrompent vos métriques :

columns:
- name: order_value
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 1000000
- name: conversion_rate
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 1
- name: event_date
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: "'2020-01-01'"
max_value: "current_date()"

Notez que les valeurs de chaînes et de dates doivent être entourées de guillemets dans des guillemets.

expect_column_mean_to_be_between

Ce test détecte les changements de distribution. Vos valeurs individuelles peuvent toutes être valides, mais si votre moyenne chute soudainement de 50%, quelque chose ne va pas :

columns:
- name: order_value
tests:
- dbt_expectations.expect_column_mean_to_be_between:
min_value: 50
max_value: 200

Interrogez d’abord vos données pour établir des bornes raisonnables. Ce test sert à détecter les anomalies, pas à imposer des valeurs exactes.

Validation multi-colonnes

expect_compound_columns_to_be_unique

Le unique natif ne fonctionne que sur des colonnes simples. Pour les clés primaires composites, vous avez besoin de ceci :

models:
- name: mrt__sales__order_lines
tests:
- dbt_expectations.expect_compound_columns_to_be_unique:
column_list: ["order_id", "line_item_id"]

expect_column_pair_values_A_to_be_greater_than_B

Validez la logique métier qui s’étend sur plusieurs colonnes :

models:
- name: mrt__finance__subscriptions
tests:
- dbt_expectations.expect_column_pair_values_A_to_be_greater_than_B:
column_A: end_date
column_B: start_date
or_equal: true
row_condition: "end_date is not null"

Autres cas d’usage : shipped_date > order_date, total_amount >= subtotal, updated_at >= created_at.

Tests de complétude

expect_row_values_to_have_data_for_every_n_datepart

Détectez les trous dans les séries temporelles. S’il vous manque une journée entière d’événements GA4, ce test échoue :

columns:
- name: event_date
tests:
- dbt_expectations.expect_row_values_to_have_data_for_every_n_datepart:
date_col: event_date
date_part: day
test_start_date: "'2024-01-01'"
test_end_date: "current_date() - 1"

Spécifiez toujours des bornes de dates. Sans elles, ce test scanne toute votre table et peut être coûteux sur de grands ensembles de données.

Le super-pouvoir row_condition

Presque tous les tests de dbt-expectations supportent un paramètre row_condition. Cela vous permet d’appliquer des tests conditionnellement sans écrire de SQL personnalisé.

Testez que account_id n’est pas null, mais uniquement pour les abonnements actifs :

columns:
- name: account_id
tests:
- dbt_expectations.expect_column_values_to_not_be_null:
row_condition: "subscription_status = 'active'"

Validez le format email uniquement là où l’email existe :

columns:
- name: email
tests:
- dbt_expectations.expect_column_values_to_match_regex:
regex: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'
row_condition: "email is not null"

Vérifiez les plages de valeurs pour des segments spécifiques :

columns:
- name: order_value
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 50000
row_condition: "country_code = 'FR' and order_status = 'completed'"

Ce paramètre seul justifie l’installation du package. Il élimine le besoin de dizaines de tests personnalisés.

Patterns d’implémentation pour BigQuery

Où placer les tests

Structurez vos tests par couche :

Sources et modèles de base : Concentrez-vous sur la fraîcheur, la validation de schéma et les vérifications de format basiques. C’est là que vous détectez les problèmes au plus près de la source.

models/base/ga4/_ga4__models.yml
models:
- name: base__ga4__events
tests:
- dbt_expectations.expect_row_values_to_have_recent_data:
datepart: hour
interval: 48
columns:
- name: user_pseudo_id
tests:
- dbt_expectations.expect_column_values_to_match_regex:
regex: '^[0-9]+\\.[0-9]+$'

Modèles intermédiaires : Concentrez-vous sur l’intégrité des jointures et la validation des transformations. Vérifiez que vos jointures ne perdent pas ou ne dupliquent pas de lignes de manière inattendue.

Marts : Concentrez-vous sur les règles métier et les contrôles de cohérence des agrégations. Ces tests protègent vos outputs finaux.

models/marts/marketing/_marketing__models.yml
models:
- name: mrt__marketing__campaign_performance
columns:
- name: roas
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 100
row_condition: "spend > 0"

Configuration de la sévérité

Tous les échecs de tests ne devraient pas bloquer votre pipeline. Utilisez severity: warn pour les tests qui nécessitent une investigation mais ne devraient pas arrêter la production :

columns:
- name: order_value
tests:
- dbt_expectations.expect_column_mean_to_be_between:
min_value: 50
max_value: 200
config:
severity: warn

Réservez severity: error (la valeur par défaut) pour les échecs critiques : violations de clé primaire, fraîcheur sur les tables critiques, données qui casseraient les systèmes en aval.

Considérations de performance

Certains tests peuvent être coûteux sur de grandes tables BigQuery. Voici comment gérer les coûts.

Utilisez row_condition avec les colonnes de partition. Si votre table est partitionnée par date, filtrez toujours :

- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 1000000
row_condition: "event_date >= current_date() - 30"

Exécutez les tests coûteux uniquement en production :

- dbt_expectations.expect_column_mean_to_be_between:
min_value: 50
max_value: 200
config:
enabled: "{{ target.name == 'prod' }}"

Taguez les tests lents pour des exécutions séparées :

- dbt_expectations.expect_row_values_to_have_data_for_every_n_datepart:
date_col: event_date
date_part: day
config:
tags: ['slow', 'daily']

Puis en CI, exécutez uniquement les tests rapides : dbt test --exclude tag:slow

Les tests les plus coûteux sont généralement expect_row_values_to_have_data_for_every_n_datepart, les tests statistiques comme expect_column_mean_to_be_between, et tout test qui ne filtre pas sur les colonnes de partition.

Exemple concret : qualité des données GA4 et ads

Voici un exemple complet couvrant un modèle de base d’événements GA4 et un mart de performance publicitaire (modèles typiques dans un projet d’analytics marketing).

Modèle de base des événements GA4

version: 2
models:
- name: base__ga4__events
description: "Événements GA4 de base avec nettoyage basique appliqué"
tests:
# La table doit avoir des données récentes (en tenant compte du délai de 24-48h de GA4)
- dbt_expectations.expect_row_values_to_have_recent_data:
datepart: hour
interval: 48
# Pas de jours manquants dans la série temporelle
- dbt_expectations.expect_row_values_to_have_data_for_every_n_datepart:
date_col: event_date
date_part: day
test_start_date: "date_sub(current_date(), interval 90 day)"
test_end_date: "date_sub(current_date(), interval 2 day)"
columns:
- name: event_id
description: "Identifiant unique de l'événement"
tests:
- unique
- not_null
- name: event_timestamp
description: "Timestamp de l'événement en UTC"
tests:
- not_null
- dbt_expectations.expect_column_values_to_be_between:
min_value: "'2020-01-01 00:00:00'"
max_value: "current_timestamp()"
- name: event_name
description: "Nom de l'événement GA4"
tests:
- not_null
- dbt_expectations.expect_column_values_to_match_regex:
regex: '^[a-z_]+$'

Mart de performance publicitaire

version: 2
models:
- name: mrt__marketing__ads_performance
description: "Performance publicitaire quotidienne par campagne"
tests:
# Clé primaire composite
- dbt_expectations.expect_compound_columns_to_be_unique:
column_list: ["date", "platform", "campaign_id"]
# Doit avoir des données récentes
- dbt_expectations.expect_row_values_to_have_recent_data:
datepart: day
interval: 2
columns:
- name: date
tests:
- not_null
- dbt_expectations.expect_column_values_to_be_between:
min_value: "'2023-01-01'"
max_value: "current_date()"
- name: platform
tests:
- not_null
- accepted_values:
values: ['google_ads', 'meta_ads', 'tiktok_ads', 'linkedin_ads']
- name: campaign_id
tests:
- not_null
- name: impressions
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 1000000000
- name: clicks
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 100000000
- name: spend
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 10000000
# Avertir si la dépense quotidienne moyenne semble anormale
- dbt_expectations.expect_column_mean_to_be_between:
min_value: 10
max_value: 100000
row_condition: "date >= date_sub(current_date(), interval 30 day)"
config:
severity: warn
- name: conversions
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 1000000
- name: roas
description: "Retour sur dépense publicitaire"
tests:
# Le ROAS devrait être entre 0 et 100 (là où il est calculable)
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 100
row_condition: "spend > 0"
config:
severity: warn

Quelles sont les autres options

dbt-expectations n’est pas le seul package de tests. Voici comment il se compare aux alternatives :

dbt-utils inclut environ 15 tests en plus de ses macros utilitaires. Vous y trouverez unique_combination_of_columns, expression_is_true, recency, et d’autres. Il y a un certain chevauchement avec dbt-expectations, mais ils se complètent bien. Utilisez les deux ; il n’y a pas de conflit.

Elementary adopte une approche différente. Au lieu de seuils fixes (“la moyenne doit être entre 50 et 200”), Elementary apprend ce qui est normal à partir de vos données historiques et alerte lorsque les valeurs dévient. Il fournit également des dashboards d’observabilité. Envisagez Elementary lorsque vous voulez une détection d’anomalies sans définir manuellement des seuils. Utilisez dbt-expectations lorsque vous avez des règles métier spécifiques à appliquer.

Les tests génériques personnalisés restent précieux pour la logique spécifique au métier qui ne correspond pas aux tests pré-construits. Même avec dbt-expectations installé, vous aurez occasionnellement besoin d’un test personnalisé pour une exigence unique.

Pour commencer : vos trois premiers tests

Si vous installez dbt-expectations aujourd’hui, commencez par ces trois tests sur votre modèle le plus critique :

1. Fraîcheur : Ajoutez expect_row_values_to_have_recent_data sur la colonne timestamp de votre mart principal. Cela détecte les données obsolètes avant que quiconque ne remarque que le dashboard affiche les chiffres d’hier.

2. Format : Ajoutez expect_column_values_to_match_regex sur une colonne d’identifiant clé. Cela détecte immédiatement les changements de format en amont.

3. Plage : Ajoutez expect_column_values_to_be_between sur une colonne KPI numérique. Cela détecte les valeurs impossibles avant qu’elles ne corrompent vos métriques.

Ces trois tests seuls détecteront des problèmes que les tests natifs de dbt manquent complètement. Étendez à partir de là au fur et à mesure que vous identifiez ce qui casse dans vos données spécifiques.

Conclusion

dbt-expectations comble le fossé entre les tests de données basiques et la qualité des données en production. Il vous donne les tests que dbt natif aurait dû inclure : validation de patterns, validation statistique, vérifications multi-colonnes et fraîcheur sur n’importe quel modèle.

Plus important encore, il complète la stratégie de tests que vous avez commencé à construire dans les articles 1-3. Les tests unitaires vérifient que votre logique de transformation est correcte. dbt-expectations vérifie que vos données réelles sont saines. Ensemble, ils détectent les problèmes aux deux points de contrôle : les bugs de code avant le déploiement, les problèmes de données pendant les exécutions en production.

Installez le package, ajoutez trois tests à votre modèle le plus critique, et lancez dbt test. Vous détecterez probablement quelque chose que vous ne saviez pas être cassé.


Référence rapide

TestCas d’usage
expect_row_values_to_have_recent_dataContrôles de fraîcheur sur n’importe quel modèle
expect_table_row_count_to_equal_other_tableVérifier que les transformations ne perdent pas de lignes
expect_table_row_count_to_be_betweenDétecter les anomalies de volume
expect_column_values_to_match_regexValidation de format (emails, IDs)
expect_column_values_to_be_betweenVérifications de plage de valeurs
expect_column_mean_to_be_betweenContrôles de cohérence de distribution
expect_compound_columns_to_be_uniqueClés primaires composites
expect_column_pair_values_A_to_be_greater_than_BValidation de relation entre colonnes
expect_row_values_to_have_data_for_every_n_datepartComplétude des séries temporelles