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.
| Aspect | Tests unitaires (dbt 1.8+) | dbt-expectations |
|---|---|---|
| Ce qui est testé | La logique de transformation | La qualité des données |
| Données en entrée | Des fixtures mockées que vous définissez | Les données de production réelles |
| Quand il s’exécute | Pipeline CI sur les changements de code | Chaque exécution de dbt build/test |
| Ce qu’il détecte | Bugs logiques, cas limites | Anomalies 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 → DashboardLes 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: 24Ce 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: 100000Validation 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: 200Interrogez 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: - 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: - 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: warnRé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: warnQuelles 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
| Test | Cas d’usage |
|---|---|
expect_row_values_to_have_recent_data | Contrôles de fraîcheur sur n’importe quel modèle |
expect_table_row_count_to_equal_other_table | Vérifier que les transformations ne perdent pas de lignes |
expect_table_row_count_to_be_between | Détecter les anomalies de volume |
expect_column_values_to_match_regex | Validation de format (emails, IDs) |
expect_column_values_to_be_between | Vérifications de plage de valeurs |
expect_column_mean_to_be_between | Contrôles de cohérence de distribution |
expect_compound_columns_to_be_unique | Clés primaires composites |
expect_column_pair_values_A_to_be_greater_than_B | Validation de relation entre colonnes |
expect_row_values_to_have_data_for_every_n_datepart | Complétude des séries temporelles |