ServicesÀ proposNotesContact Me contacter →
EN FR
Note

Unit tests vs tests de données dans dbt

Le modèle à deux points de contrôle pour les tests dbt — les unit tests bloquent les déploiements en vérifiant la logique de transformation, les tests de données bloquent la production en vérifiant la santé des données.

Planté
dbttestingdata quality

dbt propose deux catégories de tests fondamentalement différentes, qui répondent à des questions différentes. Les unit tests demandent « ma logique de transformation fonctionne-t-elle ? » Les tests de données demandent « mes données de production sont-elles saines ? » Confondre les deux conduit à des stratégies de test soit redondantes, soit truffées de lacunes.

Les deux points de contrôle

Considérons le pipeline de données comme ayant deux portes :

Modifications du code → Unit tests → Déploiement → Tests de données → Dashboard

Les unit tests bloquent les déploiements. Ils s’exécutent en CI lors des push de code. Ils utilisent des entrées mockées que l’on définit, et non des données réelles. Ils vérifient que les transformations SQL produisent la sortie attendue pour des scénarios connus. Si un unit test échoue, la modification de code n’est pas déployée.

Les tests de données bloquent les données. Ils s’exécutent à chaque exécution de dbt build ou dbt test contre les données de production réelles. Ils vérifient que les données circulant dans les modèles sont saines, fraîches et dans les limites attendues. Si un test de données échoue, les modèles en aval sont bloqués (si la sévérité est error) ou un avertissement est consigné (si la sévérité est warn).

Les deux sont nécessaires. Ils ne sont pas redondants — ils détectent des catégories de problèmes entièrement différentes.

Ce que chaque type détecte

AspectUnit tests (dbt 1.8+)Tests de données (génériques + dbt-expectations)
Ce qu’il testeLogique de transformationQualité des données
Données d’entréeFixtures mockées définies manuellementDonnées de production réelles
Quand il s’exécutePipeline CI lors des modifications de codeChaque dbt build/test run
Ce qu’il détecteBugs de logique, cas limitesAnomalies de données, problèmes source
Question type« Mon CASE WHEN catégorise-t-il correctement ? »« Toutes les valeurs sont-elles dans la plage attendue ? »

Les unit tests sont des outils à haute précision et portée étroite. On les écrit pour 5 à 10 % des modèles à logique complexe : branches CASE WHEN élaborées, fonctions de fenêtrage, calculs de dates, parsing de chaînes. Ils sont coûteux à écrire et à maintenir, mais détectent des bugs qu’aucun test de données ne trouverait.

Les tests de données sont des outils à large couverture et précision moindre. On les applique à tous les modèles : vérifications de clé primaire, surveillance de la fraîcheur, validation des plages de valeurs, application des formats. Ils sont rapides à configurer (quelques lignes de YAML) et détectent des problèmes que du code correct ne peut pas prévenir.

L’exemple concret

Considérons un modèle qui calcule le chiffre d’affaires en multipliant la quantité par le prix unitaire.

Le unit test vérifie que la logique de multiplication fonctionne :

unit_tests:
- name: test_revenue_calculation
model: mrt__sales__orders
given:
- input: ref('base__shopify__orders')
rows:
- {order_id: 1, quantity: 3, unit_price: 10.00}
- {order_id: 2, quantity: 1, unit_price: 99.99}
expect:
rows:
- {order_id: 1, revenue: 30.00}
- {order_id: 2, revenue: 99.99}

Cela prouve que 3 * 10.00 = 30.00. La logique est correcte. Mais cela ne dit rien sur les données réelles en production.

Le test de données vérifie que les valeurs de chiffre d’affaires résultantes en production sont cohérentes :

models:
- name: mrt__sales__orders
columns:
- name: revenue
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 10000000
- dbt_expectations.expect_column_mean_to_be_between:
min_value: 50
max_value: 500
config:
severity: warn

Cela détecte le problème quand un système source envoie soudainement des quantités négatives, quand une erreur de tarification produit des commandes à 0,01 € pour des produits premium, ou quand une migration de données remplit la quantité par NULL et que la multiplication produit silencieusement un chiffre d’affaires NULL. Le SQL est parfait. Les données sont cassées.

Quand les unit tests passent mais les tests de données échouent

C’est le scénario qui justifie d’avoir les deux :

  • Un système source commence à envoyer des NULL là où il n’en envoyait pas. Les unit tests passent (les données mockées n’incluent pas de NULL dans cette colonne). Les tests de données le détectent.
  • Un batch quotidien arrive six heures en retard. Les unit tests ne sont pas affectés (ils n’utilisent pas de données réelles). Le test de fraîcheur le détecte.
  • Une équipe en amont change sa tarification de dollars en centimes sans prévenir. Les unit tests passent (3 * 10.00 vaut toujours 30.00). Le test expect_column_mean_to_be_between détecte le décalage de 100x dans le chiffre d’affaires moyen.
  • Un fournisseur API commence à retourner des enregistrements dupliqués. Les unit tests passent. Le test unique sur la clé primaire détecte les doublons avant qu’ils ne causent des explosions de jointures en aval.

Dans chaque cas, le code est correct. Les données ne le sont pas. Les unit tests ne protègent pas ici car ils ne voient pas les données réelles. Les tests de données, si.

Quand les tests de données passent mais que les unit tests auraient détecté le bug

L’inverse se produit aussi :

  • On refactorise une instruction CASE WHEN et on échange accidentellement deux catégories. La distribution des données semble normale (mêmes plages, mêmes moyennes), donc les tests de données passent. Un unit test avec des sorties attendues explicites pour chaque catégorie l’aurait détecté.
  • On change une fonction de fenêtrage de ROWS BETWEEN UNBOUNDED PRECEDING à ROWS BETWEEN 1 PRECEDING, et le total courant devient une somme glissante sur deux lignes. Pour la plupart des lignes, les chiffres semblent plausibles. Un unit test ciblant un scénario multi-lignes spécifique exposerait l’erreur immédiatement.
  • On introduit une erreur de décalage dans un calcul de frontière de date. Les enregistrements proches des limites mensuelles sont mal classifiés, mais ils représentent une infime fraction du total, donc les tests de données agrégés ne le remarquent pas. Un unit test ciblant le cas limite le détecte.

Conseils pratiques

Les unit tests appartiennent au CI uniquement. Ils utilisent des données mockées et n’apportent aucune valeur en production. Les exclure des exécutions de production :

Terminal window
dbt build --exclude-resource-type unit_test

Les tests de données appartiennent à la production. Ils ont besoin de données réelles pour être pertinents. Les exécuter en CI contre un jeu de données de développement de 100 lignes n’a qu’une utilité marginale. La pleine valeur vient de leur exécution contre des données à l’échelle de la production à chaque build.

Commencer par les tests de données. Ils sont plus rapides à mettre en place (configuration YAML, pas d’écriture de fixtures), plus larges en couverture, et détectent la classe de défaillances la plus courante (problèmes de données, non de logique). Ajouter unique + not_null sur chaque clé primaire, dbt-expectations pour les plages de valeurs et les patterns, et des vérifications de fraîcheur sur les modèles critiques.

Ajouter les unit tests de façon sélective. Les réserver aux modèles dont la logique de transformation est suffisamment complexe pour que des bugs soient plausibles et que les tests de données seuls ne suffiraient pas à les détecter. Le consensus de la communauté est qu’environ 5 à 10 % des modèles justifient des unit tests.

Les unit tests vérifient que la logique de transformation est correcte. Les tests de données vérifient que les données réelles sont saines. Aucun des deux n’est suffisant seul — les unit tests détectent les bugs de code avant le déploiement ; les tests de données détectent les problèmes de données lors des exécutions en production.