dbt dispose de cinq mécanismes distincts pour valider les données et la logique : tests génériques, tests singuliers, tests unitaires, contrats de modèle et tests basés sur des packages. Chacun répond à une question différente.
Tests génériques
Les tests génériques sont des assertions paramétrées et réutilisables déclarées en YAML. dbt livre quatre tests génériques intégrés qui forment la base incontournable de tout projet.
unique valide qu’une colonne ne contient pas de valeurs en double. Appliquez-le à chaque clé primaire sans exception. Les doublons dans les clés primaires causent des explosions de jointures en aval qui sont douloureuses à diagnostiquer.
not_null s’assure que les champs requis sont renseignés. Associez-le toujours à unique sur les clés primaires.
accepted_values contraint les colonnes catégorielles à une liste explicite. Particulièrement précieux sur les champs de statut où une valeur en amont inattendue (par exemple, "cancelled" orthographié différemment) casse silencieusement la logique CASE WHEN en aval.
relationships valide l’intégrité référentielle, confirmant que les valeurs de clé étrangère existent dans leur table parente. Utilisez-le sur les jointures critiques pour détecter les enregistrements orphelins avant qu’ils ne produisent des NULL dans les rapports.
models: - name: mrt__sales__orders columns: - name: order_id data_tests: - unique - not_null - name: customer_id data_tests: - not_null - relationships: to: ref('mrt__sales__customers') field: customer_id - name: status data_tests: - accepted_values: values: ['pending', 'confirmed', 'shipped', 'delivered', 'cancelled']Vous pouvez également écrire des tests génériques personnalisés sous forme de macros dans tests/generic/ en utilisant la syntaxe de bloc {% test %}. Des exemples courants : is_positive pour les colonnes numériques, date_not_in_future pour les timestamps, not_empty_string pour les champs texte où les chaînes vides sont aussi mauvaises que les NULL.
Tests singuliers
Les tests singuliers sont des fichiers SQL autonomes dans le répertoire tests/. La requête renvoie les lignes qui violent une règle métier ; zéro ligne signifie un succès.
-- tests/assert_no_orphaned_orders.sqlselect o.order_idfrom {{ ref('mrt__finance__orders') }} oleft join {{ ref('mrt__core__customers') }} c on o.customer_id = c.customer_idwhere c.customer_id is nullUtilisez les tests singuliers quand la logique de validation est trop complexe pour les paramètres d’un test générique, nécessite des assertions cross-tables ou est propre à un modèle spécifique. Si vous écrivez le même test singulier pour plusieurs modèles, promouvez-le en test générique personnalisé.
Tests unitaires
Les tests unitaires natifs (dbt 1.8+) valident la logique de transformation avec des entrées mockées avant que les données ne touchent l’entrepôt. Ils répondent à « mon SQL est-il correct ? » plutôt qu’à « mes données sont-elles saines ? ».
unit_tests: - name: test_discount_calculation model: mrt__finance__orders given: - input: ref('base__shopify__orders') rows: - {order_id: 1, subtotal: 100, discount_code: "SAVE20"} expect: rows: - {order_id: 1, discount_amount: 20, final_total: 80}Le bloc given mock les refs d’entrée avec des données statiques. Le bloc expect définit ce que le modèle devrait sortir. dbt construit le modèle en utilisant uniquement les entrées mockées et compare la sortie réelle à la sortie attendue. Aucune donnée d’entrepôt n’est interrogée.
Les tests unitaires sont des outils à fort effort et haute précision. Le consensus de la communauté est qu’environ 5 à 10% des modèles les méritent, spécifiquement ceux avec :
- Des branches CASE WHEN complexes où chaque chemin nécessite une vérification explicite
- Des fonctions fenêtre (classement, totaux cumulés, lag/lead) qui sont notoirement délicates
- Des calculs de date avec des cas limites comme les frontières d’année fiscale ou les conversions de fuseau horaire
- Du parsing de chaînes ou des regex où les cas limites sont faciles à manquer
Pour les modèles incrémentiels, les tests unitaires deviennent particulièrement précieux. Vous pouvez surcharger la macro is_incremental() et mocker this (l’état actuel de la table) pour vérifier que la logique de merge gère correctement la déduplication, les données en retard et la frontière entre le rafraîchissement complet et les chemins incrémentiels.
Excluez les tests unitaires des exécutions de production (dbt build --exclude-resource-type unit_test) puisqu’ils utilisent des données mockées et n’apportent aucune valeur là. Ils appartiennent à la CI et au développement uniquement.
Contrats de modèle
Les contrats imposent des garanties de schéma au moment du build. Quand contract.enforced est vrai, dbt refuse de matérialiser un modèle si ses colonnes de sortie et leurs types ne correspondent pas à la déclaration YAML. Contrairement aux tests qui valident après coup, les contrats empêchent la table d’être créée du tout.
models: - name: mrt__sales__customers config: contract: enforced: true columns: - name: customer_id data_type: int64 constraints: - type: not_null - type: primary_key - name: email data_type: string - name: lifetime_value data_type: numericUne nuance critique : l’application des contraintes varie selon l’entrepôt. BigQuery et Snowflake traitent les contraintes primary_key et unique uniquement comme des métadonnées ; ils ne rejettent pas les mauvaises données au moment de l’insertion. Seul not_null est appliqué de façon fiable entre les plateformes. Déclarez les contraintes informatives pour la documentation et l’optimisation des requêtes, mais associez-les toujours à des tests dbt réels pour la validation.
Appliquez les contrats uniquement aux modèles mart exposés publiquement : les tables que les outils BI interrogent, les datasets alimentant le reverse ETL, les modèles partagés avec d’autres équipes. Les contrats sur les modèles base ou intermediate dans l’architecture trois couches ajoutent de la complexité sans bénéfice significatif. Combinés avec les versions de modèles, les contrats permettent une évolution de schéma non destructive où vous pouvez introduire une v2 tout en maintenant la v1 pour les consommateurs existants.
Tests basés sur des packages
Trois packages étendent les capacités de test de dbt de façon distincte.
dbt-utils
Le package utilitaire officiel de dbt Labs fournit des tests qui devraient probablement être intégrés nativement :
unique_combination_of_columns: Unicité de la clé composite. Essentiel quand votre grain est défini par plusieurs colonnes.expression_is_true: Tester n’importe quelle expression SQL. Le couteau suisse pour les assertions personnalisées commequantity > 0ouend_date >= start_date.recency: Vérifier la fraîcheur des données sur n’importe quel modèle sans vérifications de fraîcheur de source.equal_rowcount: Comparer les comptages de lignes entre deux relations. Inestimable lors du refactoring pour confirmer que les transformations ne suppriment pas de lignes.
dbt-expectations
Porté depuis la bibliothèque Python Great Expectations, ce package (maintenant maintenu par Metaplane) fournit 60+ tests pour la validation statistique et basée sur des patterns.
Les tests à plus haute valeur :
expect_column_values_to_be_between: Validation de plage. Détecte les valeurs impossibles (revenu négatif, taux de conversion supérieur à 1,0) avant qu’elles ne corrompent les métriques.expect_column_values_to_match_regex: Validation de pattern pour les emails, SKUs, numéros de téléphone ou tout format avec une structure connue.expect_column_mean_to_be_between: Détecte les décalages de distribution. Les valeurs individuelles peuvent être valides, mais une chute de 50% dans la valeur moyenne des commandes signale un problème.expect_row_values_to_have_recent_data: Vérifications de fraîcheur sur n’importe quel modèle, pas seulement les sources. Cela seul justifie l’installation du package.expect_compound_columns_to_be_unique: Validation de clé primaire composite.
Le paramètre row_condition est la fonctionnalité phare du package. Il permet d’appliquer n’importe quel test conditionnellement sans SQL personnalisé : tester que shipping_date n’est pas null uniquement où status = 'shipped', ou valider le format d’email uniquement où email is not null.
Elementary
Elementary adopte une approche fondamentalement différente : détection d’anomalies plutôt que seuils statiques. Plutôt que de définir « échouer si plus de 100 nulls », Elementary apprend les patterns depuis vos données historiques et alerte quand les métriques dévient au-delà des plages attendues en utilisant des calculs de Z-score.
volume_anomalies: Alerte quand les comptages de lignes dévient des patterns historiques. Pas besoin de deviner des seuils qui dérivent au fur et à mesure que les données croissent.freshness_anomalies: Surveille le délai entre les mises à jour de façon adaptative.column_anomalies: Suit les métriques de niveau colonne (moyenne, nombre de nulls, cardinalité) et signale les déviations.schema_changes: Détecte les ajouts, suppressions ou changements de type de colonnes inattendus.
Elementary est idéal pour détecter les « inconnues inconnues » — les anomalies pour lesquelles vous n’auriez pas pensé à écrire des tests explicites.
Quand utiliser quoi
La décision commence par une question : testez-vous la logique ou les données ?
| Vous devez vérifier… | Utilisez |
|---|---|
| La logique de transformation est correcte | Test unitaire |
| Les clés primaires sont uniques et non nulles | Tests génériques (unique + not_null) |
| Les clés étrangères référencent des parents valides | Test générique (relationships) |
| Des règles métier complexes cross-tables | Test singulier |
| La stabilité du schéma pour les consommateurs | Contrat de modèle |
| Les plages de valeurs, patterns, distributions | dbt-expectations |
| Les anomalies que vous ne pouvez pas prédire | Elementary |
| La migration produit des résultats identiques | dbt-audit-helper |
L’intensité des tests doit augmenter vers les bords de votre DAG. Les sources sont là où les problèmes entrent ; les marts sont là où ils atteignent les consommateurs. Les modèles base ont besoin de tests de clé primaire et de vérifications de format de base. Les modèles intermediate ont besoin de validation de l’intégrité des jointures. Les marts méritent l’investissement le plus lourd : tests unitaires pour la logique complexe, tests génériques complets, contrats pour la stabilité du schéma et dbt-expectations pour l’application des règles métier.
Le chemin de maturité
Démarrage (0-50 modèles) : unique + not_null sur chaque clé primaire. relationships sur les clés étrangères critiques. Vérifications de fraîcheur des sources. Toute la sévérité définie sur error.
Croissance (50-200 modèles) : Ajoutez dbt-utils et dbt-expectations pour les lacunes de couverture. Introduisez la sévérité conditionnelle (error en CI, warn en production). Écrivez des tests unitaires pour les 3 à 5 modèles avec la logique métier la plus complexe. Taguez les modèles avec meta.owner pour le routage des alertes.
Maturité (200+ modèles) : Déployez Elementary pour la détection d’anomalies sur les tables métier clés. Activez les contrats sur les marts exposés publiquement. Construisez des alertes par palier : page l’astreinte pour les échecs critiques, Slack pour les priorités élevées, digest quotidien pour les priorités moyennes. Convertissez chaque incident en production en test permanent, en élargissant continuellement la couverture basée sur les vrais échecs plutôt que sur la spéculation.
Les tests qui échouent chroniquement dégradent le rapport signal/bruit de la suite de tests. Un test en échec doit soit être corrigé, avoir son seuil mis à jour, soit être supprimé.