Les modèles incrémentaux ont deux chemins d’exécution — un pour le full-refresh, un autre pour les chargements incrémentaux normaux — et les équipes ne testent généralement qu’un seul. Le chemin du full-refresh est testé parce que c’est ce que fait dbt run pendant le développement.
Un mode de défaillance courant : le modèle fonctionne en dev et en full-refresh, mais un bug dans la clause WHERE du chemin incrémental produit des doublons qui s’accumulent sur des mois d’exécutions quotidiennes avant que quelqu’un ne le remarque.
Les tests unitaires détectent ces bugs avant le déploiement en simulant les deux chemins d’exécution avec des données de test contrôlées.
Le pattern de test dual
Chaque modèle incrémental nécessite au moins deux tests unitaires : un pour le mode full-refresh, un pour le mode incrémental. Ce n’est pas optionnel. Si vous ne testez qu’un seul mode, vous laissez la moitié de votre logique non testée.
-- models/intermediate/int__events_processed.sql{{ config(materialized='incremental', unique_key='event_id') }}
select event_id, user_id, event_type, event_timestamp, processed_atfrom {{ ref('base__ga4__events') }}
{% if is_incremental() %}where event_timestamp > (select max(event_timestamp) from {{ this }}){% endif %}Le bloc {% if is_incremental() %} est là où se cachent les bugs. Les erreurs courantes incluent les erreurs d’un entier (>= vs >), les décalages de fuseau horaire, et les références de colonnes incorrectes.
Tester le full-refresh
unit_tests: - name: test_int_events_processed_full_refresh model: int__events_processed description: "En full-refresh, toutes les lignes sources devraient être traitées" overrides: macros: is_incremental: false given: - input: ref('base__ga4__events') rows: - {event_id: 1, user_id: 100, event_type: "click", event_timestamp: "2024-06-01 10:00:00"} - {event_id: 2, user_id: 100, event_type: "purchase", event_timestamp: "2024-06-02 11:00:00"} - {event_id: 3, user_id: 101, event_type: "click", event_timestamp: "2024-06-03 12:00:00"} expect: rows: - {event_id: 1, user_id: 100, event_type: "click"} - {event_id: 2, user_id: 100, event_type: "purchase"} - {event_id: 3, user_id: 101, event_type: "click"}Définir is_incremental: false dans les overrides simule dbt run --full-refresh. Les trois événements apparaissent dans la sortie — aucun filtrage ne se produit.
Tester le mode incrémental
unit_tests: - name: test_int_events_processed_incremental model: int__events_processed description: "En exécution incrémentale, seuls les nouveaux événements devraient être insérés" overrides: macros: is_incremental: true given: - input: ref('base__ga4__events') rows: - {event_id: 1, user_id: 100, event_type: "click", event_timestamp: "2024-06-01 10:00:00"} - {event_id: 2, user_id: 100, event_type: "purchase", event_timestamp: "2024-06-02 11:00:00"} - {event_id: 3, user_id: 101, event_type: "click", event_timestamp: "2024-06-03 12:00:00"} - input: this rows: - {event_id: 1, user_id: 100, event_type: "click", event_timestamp: "2024-06-01 10:00:00"} expect: rows: - {event_id: 2, user_id: 100, event_type: "purchase"} - {event_id: 3, user_id: 101, event_type: "click"}Définir is_incremental: true force le mode incrémental. input: this mocke l’état actuel de la table cible — elle contient un événement avec l’horodatage 2024-06-01 10:00:00. La clause WHERE du modèle filtre les événements après cet horodatage, donc seuls les événements 2 et 3 apparaissent.
L’insight critique : expect montre les insertions, pas l’état final
Cela piège de nombreux développeurs. Le bloc expect représente ce qui est inséré ou mergé, pas l’état final de la table. Dans le test incrémental ci-dessus, event_id 1 existe déjà dans this, donc seuls les événements 2 et 3 apparaissent dans la sortie attendue. Vous testez la logique de transformation, pas l’opération de merge elle-même.
Tester la logique de merge
La configuration merge_update_columns ajoute de la complexité. Quand une ligne avec une unique_key existante arrive, BigQuery met à jour uniquement les colonnes spécifiées, laissant les autres inchangées.
{{ config( materialized='incremental', unique_key='user_id', merge_update_columns=['email', 'updated_at']) }}unit_tests: - name: test_int_users_current_merge_update model: int__users_current description: "Les utilisateurs existants devraient avoir l'email mis à jour, pas created_at" overrides: macros: is_incremental: true given: - input: ref('base__crm__users') rows: - {user_id: 1, email: "nouveau@exemple.com", created_at: "2024-06-15", updated_at: "2024-06-15"} - input: this rows: - {user_id: 1, email: "ancien@exemple.com", created_at: "2024-01-01", updated_at: "2024-01-01"} expect: rows: - {user_id: 1, email: "nouveau@exemple.com", created_at: "2024-06-15", updated_at: "2024-06-15"}Mise en garde importante : ce test valide que votre logique de transformation produit la bonne ligne de sortie. Il ne teste pas le comportement du merge lui-même — c’est la responsabilité de BigQuery. La sortie attendue montre created_at: "2024-06-15" car c’est ce que contiennent les données sources. En pratique, le merge réel préserverait le created_at: "2024-01-01" original depuis this. Pour les scénarios de merge plus complexes, utilisez dbt-audit-helper pour comparer les résultats réels après une exécution de test.
Tester les données tardives
Les pipelines réels reçoivent rarement des événements dans un ordre chronologique parfait. Un pattern courant utilise une fenêtre de rattrapage : au lieu de ne traiter que les événements plus récents que max(event_timestamp), vous incluez également les événements qui sont arrivés récemment (sur la base d’une colonne _loaded_at) même si leur horodatage d’événement est plus ancien.
unit_tests: - name: test_int_events_processed_late_arriving model: int__events_processed description: "Les événements tardifs dans la fenêtre de rattrapage devraient être capturés" overrides: macros: is_incremental: true vars: lookback_hours: 24 given: - input: ref('base__ga4__events') rows: - {event_id: 4, event_timestamp: "2024-06-01 08:00:00", _loaded_at: "2024-06-02 10:00:00"} - {event_id: 5, event_timestamp: "2024-06-02 09:00:00", _loaded_at: "2024-06-02 09:05:00"} - input: this rows: - {event_id: 1, event_timestamp: "2024-06-01 10:00:00"} expect: rows: - {event_id: 4, event_timestamp: "2024-06-01 08:00:00"} - {event_id: 5, event_timestamp: "2024-06-02 09:00:00"}L’événement 4 a un horodatage ancien (1er juin, 08:00) mais un temps de chargement récent (2 juin, 10:00) — il est arrivé tardivement. Un simple WHERE event_timestamp > max(event_timestamp) le manquerait. La fenêtre de rattrapage garantit sa capture.
Prérequis : le schéma doit exister
Avant d’exécuter des tests unitaires incrémentaux, construisez vos modèles incrémentaux avec :
dbt run --select "config.materialized:incremental" --emptyLes tests unitaires doivent inspecter la structure de la table. Ils échoueront avec “Not able to get columns for unit test” si la table cible n’existe pas. Cette commande crée des tables vides avec le bon schéma sans traiter aucune donnée.
Remarques sur le pattern dual
Les bugs de merge incrémental s’accumulent silencieusement entre les exécutions. Le pattern dual — tester à la fois is_incremental: false et is_incremental: true avec un mocking explicite de this — détecte ces bugs avant le déploiement, quand l’état incorrect est encore récupérable.