L’intensité des tests dans un projet dbt doit augmenter vers les bords du DAG. Les sources sont là où les problèmes entrent dans le pipeline ; les marts sont là où ils atteignent les consommateurs. Appliquer la même densité de tests à chaque couche tend à produire une sur-couverture sur les modèles intermediate et une sous-couverture sur les marts.
Sources : Ne testez que ce qui est corrigeable
À la couche source, vous testez des données que vous ne contrôlez pas. La discipline est la retenue — n’ajoutez que les tests sur lesquels vous pouvez réellement agir.
La fraîcheur des sources est le test le plus précieux à cette couche. Configurez loaded_at_field et des seuils appropriés basés sur vos engagements SLA. La sévérité importe : utilisez error pour les sources critiques pour le métier où des données obsolètes sont pires qu’aucune donnée, warn pour les sources informationnelles où les retards sont tolérables.
sources: - name: salesforce freshness: warn_after: {count: 12, period: hour} error_after: {count: 24, period: hour} tables: - name: accounts loaded_at_field: systemmodstamp columns: - name: id data_tests: - unique - not_nullLes tests de clé primaire appartiennent aux sources uniquement si les doublons ou valeurs nulles dans ces clés sont réellement corrigeables en amont. Si votre système source a des enregistrements en double connus que vous ne pouvez pas supprimer, n’ajoutez pas un test unique qui échouera à chaque exécution. Gérez la déduplication dans votre modèle base et sautez le test au niveau source. Un test sur lequel vous ne pouvez pas agir est du bruit.
C’est la couche où beaucoup d’équipes over-testent. Elles ajoutent unique, not_null, accepted_values et relationships à chaque colonne source parce que « c’est ce qu’on fait avec les tests dbt ». Mais quand ces tests échouent parce que le système source envoie des données incohérentes (ce qu’il fera), il n’y a rien d’actionnable à faire. Vous accepterez soit des échecs permanents, soit vous supprimerez les tests — et la seconde option est la bonne décision.
Base : Le contrat propre
Les modèles base ont un seul travail : produire des enregistrements bien typés, dédupliqués et gérés en termes de valeurs nulles depuis les sources brutes. Les tests à cette couche font respecter le « contrat propre » — la promesse que tout ce qui est en aval reçoit des données respectant des garanties structurelles de base.
Le principe clé : ne testez pas votre nettoyage. Si votre modèle base filtre les valeurs nulles de order_id, ajouter un test not_null sur order_id est redondant. Il réussira toujours parce que vous venez de les filtrer. Cela ajoute du temps d’exécution sans ajouter de sécurité.
Ce qu’il faut tester à la place :
Les clés primaires — unique + not_null sur le grain du modèle base. Après déduplication, la clé primaire doit être propre.
Les anomalies spécifiques au métier — valeurs hors des plages acceptables, valeurs catégorielles inattendues. Celles-ci détectent les problèmes qui survivent à votre logique de nettoyage.
models: - name: base__shopify__orders columns: - name: order_id data_tests: - unique - not_null - name: order_total data_tests: - dbt_utils.expression_is_true: expression: ">= 0" - name: currency data_tests: - accepted_values: values: ['USD', 'EUR', 'GBP', 'CAD']Remarquez ce qui est absent : les tests de fraîcheur (ils appartiennent à la source), les tests d’intégrité référentielle (les clés étrangères n’ont pas encore été jointes) et les vérifications de doublons sur les colonnes autres que la clé primaire (la déduplication devrait s’en charger).
Intermediate : Les conséquences des jointures
Les modèles intermediate joignent et agrègent. Le focus des tests se déplace vers la vérification que ces opérations ont produit les résultats attendus.
Intégrité de la clé primaire après les changements de granularité. Si vous agrégez depuis les lignes de commande vers les commandes, order_id devrait maintenant être unique. Si vous joignez deux tables, le grain du modèle résultant devrait être ce que vous aviez prévu. Un test unique sur la clé primaire post-jointure est votre premier signal qu’une jointure a multiplié les lignes de façon inattendue.
models: - name: int__orders__enriched columns: - name: order_id data_tests: - unique - not_null - name: customer_id data_tests: - relationships: to: ref('int__customers__base') field: customer_idIntégrité référentielle sur les tables jointes. Le test relationships ici vérifie que vos conditions de jointure sont correctes — que le customer_id dans votre modèle de commandes existe réellement dans le modèle de clients. S’il n’existe pas, vous avez des enregistrements orphelins qui produiront des NULL en aval.
Vérifications de cohérence sur les agrégations. Si vous calculez le revenu par client, un expression_is_true sur total_revenue >= 0 détecte les erreurs de signe dans la logique d’agrégation.
La couche intermediate est là où vous détectez les explosions de jointures avant qu’elles ne corrompent les sorties mart. Une condition manquante dans une jointure qui multiplie les lignes par 10 fera paraître le revenu de votre mart 10 fois trop élevé — et le test de clé primaire au niveau intermediate est ce qui l’arrête avant qu’il n’atteigne les tableaux de bord.
Marts : Investissement maximum
La couche mart mérite l’investissement en tests le plus lourd. Ce sont les modèles que les parties prenantes interrogent, auxquels les outils BI se connectent et que les pipelines en aval consomment. Les problèmes qui atteignent cette couche sont visibles par les clients.
Tests unitaires pour la logique métier complexe. Règles de segmentation client, calculs financiers, métriques de prévision — ce sont là que les tests unitaires valent leur prix. Le 1% des colonnes qui méritent des tests unitaires est presque entièrement concentré ici.
Tests génériques complets sur toutes les colonnes exposées. Pas seulement les clés primaires — toutes les métriques clés devraient avoir des vérifications de plage, tous les champs catégoriels devraient avoir des accepted_values.
Contrats de modèle pour la stabilité du schéma. Tout modèle mart que les outils BI ou les pipelines reverse ETL interrogent devrait avoir contract: {enforced: true}. Cela empêche les changements de schéma de casser silencieusement les consommateurs.
models: - name: mrt__sales__customers config: contract: enforced: true columns: - name: customer_id data_type: int64 data_tests: - unique - not_null - name: lifetime_value data_type: numeric data_tests: - dbt_utils.expression_is_true: expression: ">= 0" - name: segment data_type: string data_tests: - accepted_values: values: ['champion', 'loyal', 'at_risk', 'new', 'prospect']Concentrez-vous sur les colonnes calculées nettes. Ne re-testez pas les champs passthrough qui ont déjà été validés en amont. Si order_id a été testé à la couche base et n’a pas changé de grain ni été joint, le tester de nouveau à la couche mart est redondant. Concentrez la couverture des tests sur les colonnes qui ont été calculées à cette couche.
La vue en couches
Sur un projet typique :
| Couche | Focus principal | Focus secondaire | À omettre |
|---|---|---|---|
| Sources | Fraîcheur, PKs si corrigeables | — | Tests non actionnables |
| Base | PKs post-dédup, anomalies | Validation de format | Re-tester le nettoyage |
| Intermediate | PKs post-jointure, intégrité référentielle | Cohérence des agrégations | Validation de contenu profonde |
| Marts | Règles métier, contrats, tests unitaires | Tests génériques complets | Re-tester les passthroughs |
Cette distribution signifie que la majorité de votre YAML de tests résidera dans la couche mart, avec une couverture plus légère mais ciblée aux sources et à la base. La couche intermediate reçoit les tests « jointure paranoïaque » — vérifications de clé primaire après chaque modèle qui change la granularité.
Si vous instrumentez un nouveau projet et ne savez pas par où commencer : ajoutez unique + not_null à chaque clé primaire à chaque couche, ajoutez la fraîcheur source sur chaque source d’ingestion et ajoutez des contrats sur chaque mart. Cette base seule détectera la majorité des incidents en production avant qu’ils n’atteignent les parties prenantes.