ServicesÀ proposNotesContact Me contacter →
EN FR
Note

Tests unitaires des consommateurs de snapshots dans dbt

Trois stratégies pour tester la logique liée aux snapshots — les modèles de base pré-snapshot, les calculs de plages de dates SCD2 dans les modèles en aval, et le hachage pour la détection des changements.

Planté
dbttestingdata modeling

Les tests unitaires natifs de dbt ne supportent pas directement les ressources de type snapshot. Vous ne pouvez pas écrire un test unitaire avec model: mon_snapshot parce que les snapshots ne sont pas des modèles — ce sont un type de ressource distinct avec leur propre chemin d’exécution.

Cette limitation a du sens quand on y réfléchit. Les snapshots détectent les changements dans le temps, en comparant l’état actuel de la source à l’état précédent du snapshot. Les tests unitaires s’exécutent en isolation avec des données mockées — il n’y a pas d‘“état précédent” à comparer.

Comment tester alors la logique liée aux snapshots ? Trois stratégies, chacune ciblant une partie différente du pipeline.

Stratégie 1 : Tester les modèles de base pré-snapshot

La transformation qui prépare les données pour un snapshot est tout aussi importante que le snapshot lui-même. Si votre modèle de staging a des bugs, le snapshot préservera fidèlement ces bugs indéfiniment.

unit_tests:
- name: test_base_crm_users_snapshot_ready
model: base__crm__users
description: "Le modèle de base devrait correctement préparer les données pour le snapshot"
given:
- input: source('crm', 'users')
rows:
- {id: 1, name: "Alice", email: "alice@example.com", _updated_at: "2024-06-01"}
- {id: 1, name: "Alice Smith", email: "alice@example.com", _updated_at: "2024-06-15"}
expect:
rows:
- {user_id: 1, user_name: "Alice", email: "alice@example.com", updated_at: "2024-06-01"}
- {user_id: 1, user_name: "Alice Smith", email: "alice@example.com", updated_at: "2024-06-15"}

Ce test vérifie que :

  • Les noms de colonnes brutes sont correctement transformés (id vers user_id, name vers user_name)
  • La colonne updated_at (que le snapshot utilisera pour la détection des changements) est correctement exposée
  • Plusieurs versions du même utilisateur sont préservées — critique pour que le SCD2 fonctionne correctement

Si le modèle de base déduplique accidentellement ou filtre les versions plus anciennes, le snapshot ne verra jamais les changements qu’il doit suivre.

Stratégie 2 : Tester les consommateurs de plages de dates SCD2

Après l’exécution du snapshot, les modèles en aval consomment typiquement la sortie du snapshot et calculent des champs dérivés comme les dates valid_to et les indicateurs is_current. C’est là qu’interviennent les fonctions de fenêtrage, et là où les bugs aiment se cacher.

-- models/intermediate/int__users_history.sql
select
user_id,
user_name,
valid_from,
coalesce(
lead(valid_from) over (partition by user_id order by valid_from),
'2199-12-31'
) as valid_to,
case
when lead(valid_from) over (partition by user_id order by valid_from) is null
then true
else false
end as is_current
from {{ ref('snp_users') }}
unit_tests:
- name: test_int_users_history_date_ranges
model: int__users_history
description: "valid_to devrait être le valid_from de la version suivante, ou 2199-12-31 pour la version actuelle"
given:
- input: ref('snp_users')
rows:
- {user_id: 1, user_name: "Alice", valid_from: "2024-01-01"}
- {user_id: 1, user_name: "Alice Smith", valid_from: "2024-06-15"}
- {user_id: 2, user_name: "Bob", valid_from: "2024-03-01"}
expect:
rows:
- {user_id: 1, user_name: "Alice", valid_from: "2024-01-01", valid_to: "2024-06-15", is_current: false}
- {user_id: 1, user_name: "Alice Smith", valid_from: "2024-06-15", valid_to: "2199-12-31", is_current: true}
- {user_id: 2, user_name: "Bob", valid_from: "2024-03-01", valid_to: "2199-12-31", is_current: true}

Ce test vérifie plusieurs choses à la fois :

  1. Calcul de la plage de dates : le premier enregistrement d’Alice a valid_to = "2024-06-15" (le valid_from de la version suivante), tandis que son second enregistrement utilise la date sentinelle lointaine.
  2. Logique de l’indicateur actuel : seule la version la plus récente de chaque utilisateur a is_current: true.
  3. Gestion de la version unique : Bob n’a qu’un seul enregistrement, donc il est automatiquement actuel avec le valid_to sentinelle.
  4. Isolation des partitions : les enregistrements d’Alice n’affectent pas les calculs de Bob — chaque user_id est une partition séparée.

C’est le genre de logique qu’il est presque impossible de vérifier par une revue de code seule. Le partitionnement de la fonction de fenêtrage, l’ordonnancement, et les spécifications de cadre interagissent de manières qui ne révèlent les bugs qu’avec de vraies données.

Stratégie 3 : Tester le hachage pour la détection des changements

Certaines équipes implémentent leur propre détection de changements en utilisant des hachages au niveau des lignes, particulièrement quand les sources n’ont pas d’horodatages updated_at fiables. Le principe : concaténer les colonnes pertinentes, hacher le résultat, comparer les hachages entre les exécutions pour détecter les changements.

Les fonctions de hachage sont sensibles à l’ordre des colonnes, à la gestion des nulls, et au cast des types de données. Un changement d’apparence cosmétique (réordonner les colonnes dans votre hachage) peut complètement casser la détection des changements.

unit_tests:
- name: test_change_detection_hash
model: base__crm__users_with_hash
description: "Le hash de ligne devrait changer quand une colonne suivie change"
given:
- input: source('crm', 'users')
rows:
- {id: 1, name: "Alice", email: "alice@example.com"}
- {id: 2, name: "Bob", email: "bob@example.com"}
expect:
rows:
- {user_id: 1, row_hash: "abc123"}
- {user_id: 2, row_hash: "def456"}

Pour écrire ce test, vous devrez calculer les valeurs de hachage attendues manuellement (ou exécuter votre modèle une fois et capturer les sorties). Le test devient ensuite une protection contre les régressions — si quelqu’un modifie accidentellement la logique de hachage, le test le détecte.

Un test complémentaire utile vérifie que le hachage change réellement quand les données changent :

unit_tests:
- name: test_change_detection_hash_sensitivity
model: base__crm__users_with_hash
description: "Le hash devrait différer quand l'email change"
given:
- input: source('crm', 'users')
rows:
- {id: 1, name: "Alice", email: "alice@example.com"}
- {id: 1, name: "Alice", email: "alice.nouveau@example.com"}
expect:
rows:
- {user_id: 1, row_hash: "abc123"}
- {user_id: 1, row_hash: "xyz789"}

Le premier test prouve que le hachage est déterministe. Le second prouve qu’il est sensible aux changements. Ensemble, ils protègent la fondation de votre pipeline de détection des changements.

Choisir une stratégie

En pratique, vous utiliserez souvent les trois :

  • Les tests pré-snapshot empêchent les mauvaises données d’entrer définitivement dans la table d’historique
  • Les tests sur les consommateurs vérifient que la logique de fenêtrage SCD2 produit des plages de dates et des indicateurs actuels corrects
  • Les tests de hachage protègent le mécanisme de détection des changements lui-même

Les tests sur les consommateurs (stratégie 2) ont typiquement la valeur la plus élevée parce que la logique de plages de dates SCD2 est complexe, difficile à vérifier à l’œil, et a un impact direct sur la façon dont les requêtes en aval filtrent les données historiques.