Dans l'article précédent, on a posé la gouvernance d'un projet partagé. On revient maintenant sur la qualité, mais sous un angle qu'on n'a pas encore vu et c'est les tests unitaires (unit tests).

Dans l'article sur les tests, on a vu les tests génériques et singuliers. Ils vérifient ta donnée réelle du style est-ce que y a-t-il des nulls, des doublons, des valeurs hors plage dans les tables. Mais ils ne disent rien de la logique. Un calcul peut tourner sans la moindre erreur et donner un résultat faux. Les unit tests, arrivés avec dbt 1.8 pour combler cela donc ils testent la logique d'un modèle sur des données fictives, comme en développement des apps.

Data tests et unit tests (les tests unitaires)

C'est le point à bien comprendre, parce que les deux portent le mot "test" mais ne font pas du tout la même chose.

Tests de données (génériques, singuliers) Unit tests
Ce qu'ils testent la donnée réelle dans tes tables la logique de transformation du modèle
Sur quelles données tes vraies données des données fictives que tu fournis
La question posée "y a-t-il des nulls ou des doublons maintenant ?" "mon SQL calcule-t-il le bon résultat ?"
Quand après le build, sur la donnée en place avant, sur des cas que tu choisis

Ce sont donc deux filets de sécurité complémentaires, pas concurrents.

Pourquoi tester la logique

Prenons un panier moyen, calculé avec total_depense / nb_commandes. Imagine qu'en refactorant, tu écrives par erreur total_depense / (nb_commandes + 1). Le SQL est valide, la requête tourne, la colonne se remplit. Les valeurs ne sont ni nulles, ni en double, ni hors plage. Elles sont juste fausses.

Un unit test attrape ça immédiatement, parce que tu lui donnes une entrée connue et tu déclares la sortie attendue. Si le calcul dévie, le test échoue, et tu le sais avant que le chiffre faux n'arrive dans un dashboard. Et c'est possible de tester des cas peu probable que la prod ne t'a pas encore envoyés.

Notre premier unit test

Un unit test se déclare en YAML, avec la clé unit_tests, dans un fichier sous models/. Comme les unit tests peuvent être longs, la bonne pratique est de les ranger dans leur propre fichier plutôt que de gonfler le _models.yml.

On va créer donc models/marts/_unit_tests.yml. On y donne le modèle visé, des entrées fictives (given) et le résultat attendu (expect).

Sur mart_clients_commandes, vérifions que le panier moyen est bien le total divisé par le nombre de commandes :

unit_tests:
  - name: test_panier_moyen
    model: mart_clients_commandes
    given:
      - input: ref('stg_customers')
        rows:
          - {customer_id: 2, prenom: 'Sophie'}
      - input: ref('stg_orders')
        rows:
          - {order_id: 1, customer_id: 2, montant: 100}
          - {order_id: 2, customer_id: 2, montant: 50}
    expect:
      rows:
        - {customer_id: 2, nb_commandes: 2, total_depense: 150, panier_moyen: 75}

On fournit un client et deux commandes, et on déclare ce que le modèle doit produire : 2 commandes, 150 de total, 75 de panier moyen.

dbt exécute la logique du modèle sur ces données inventées, sans toucher à tes vraies tables, et compare le résultat à ce que tu attends. Tu n'as besoin de renseigner que les colonnes que la logique utilise, dbt gère le reste.

Ajouter une règle métier, puis la protéger

Dans le test précédent, on comptait toutes les commandes, quel que soit leur statut. Mais pour un chiffre d'affaires et un panier moyen pertinents, on ne veut compter que les commandes réellement payées, donc les completed. Une commande pending ou annulée ne doit pas gonfler le total.

Ajoute ce filtre au niveau commandes de mart_clients_commandes.sql :

commandes as (
    select *
    from {{ ref('stg_orders') }}
    where statut = 'completed'   -- on ne compte que les commandes payées
)

On reprend le même test models/marts/_unit_tests.yml et on lui ajoute les statuts et une commande pending

unit_tests: 
  - name: test_panier_moyen
    model: mart_clients_commandes
    given:
      - input: ref('stg_customers')
        rows:
          - {customer_id: 2, prenom: 'Sophie'}
      - input: ref('stg_orders')
        rows:
          - {order_id: 1 , customer_id: 2, montant: 100, statut: 'completed'}
          - {order_id: 2 , customer_id: 2, montant: 50, statut: 'completed'}
          - {order_id: 3 , customer_id: 2, montant: 30, statut: 'pending'}
    expect:
      rows:
        - {customer_id: 2, nb_commandes: 2, total_depense: 150, panier_moyen: 75}

On déclare que le modèle ne doit compter que les deux commandes completed, pour un total de 150 et un panier moyen de 75. Les commandes pending ne doivent pas être compter dans le total.

Lancer les unit tests

Les unit tests tournent avec la commande de test habituelle :

dbt test

Ils sont aussi exécutés pendant dbt build. Et si tu veux ne lancer que les unit tests, sans les tests de données, tu cibles par type :

dbt test --select test_type:unit
💡
Si les tests ne fonctionne pas, vérifie ta version de dbt. La fonctionnalité est arrivée avec dbt 1.8 ; sur une version antérieure, la clé unit_tests n'existe pas.

Quand les utiliser

Un modèle de staging qui se contente de renommer des colonnes n'a pas de logique à tester donc un test de données suffit largement. Les unit tests sont faits pour les modèles qui contiennent de la vraie logique :

  • Des calculs (paniers, marges, taux)
  • Des CASE un peu complexes
  • Des window functions
  • Des extractions par expression régulière
  • Des agrégations délicates

La suite ?

Dans le prochain article, ces tests vont prendre tout leur sens car on va mettre en place la CI/CD et le Slim CI, pour que tests, unit tests et build tournent automatiquement à chaque modification, et ne reconstruisent que ce qui a changé.


Aller plus loin

Cet article fait partie de la formation dbt complète, du premier modèle au déploiement en production.

👉 Suivre toute la formation dbt

dbt tourne sur Snowflake dans ce parcours. Pour maîtriser le socle (warehouses, rôles, ingestion) :

👉 Accéder à la Formation Snowflake

Et pour t'entraîner en conditions d'examen, la certification dbt Analytics Engineering teste précisément les tests et les unit tests.

👉 Préparer la certification dbt sur DataCertification.fr

Tu veux que je t'accompagne sur ton projet data (Snowflake, dbt, modélisation, industrialisation) ?

👉 Réserver un appel de 30 minutes


Questions fréquentes

C'est quoi un unit test en dbt ?

Un unit test est un test qui vérifie la logique d'un modèle sur des données fictives que tu fournis, au lieu de vérifier la donnée réelle. Tu donnes des lignes d'entrée et le résultat attendu, et dbt exécute la transformation du modèle sur ces données inventées pour comparer le résultat. C'est l'équivalent d'un test unitaire en dev logiciel.

Quelle différence entre un data test et un unit test dbt ?

Un test de données (générique ou singulier) vérifie l'état de ta donnée réelle comme nulls, doublons, valeurs hors plage. Un unit test vérifie que la logique de transformation produit le bon résultat, sur des données fictives que tu choisis. Le premier répond à "la donnée en base est-elle saine ?", le second à "mon SQL calcule-t-il juste ?". Ils sont complémentaires.

Comment écrire un unit test dbt ?

Dans un fichier YAML, sous la clé unit_tests. On indique le nom du test, le modèle visé, les entrées fictives sous given (une liste d'inputs avec leurs lignes), et le résultat attendu sous expect. dbt exécute la logique du modèle sur les données fournies et compare la sortie à ce qui est attendu. On ne renseigne que les colonnes utiles à la logique testée.

Comment lancer uniquement les unit tests ?

Avec la commande dbt test, à laquelle on ajoute une sélection par type : dbt test --select test_type:unit. Cela exécute seulement les unit tests, sans les tests de données. Les unit tests sont aussi lancés automatiquement pendant un dbt build.

Faut-il un unit test sur chaque modèle ?

Non. Un modèle de staging qui renomme simplement des colonnes n'a pas de logique à tester, un test de données suffit. Les unit tests sont faits pour les modèles avec une vraie logique : calculs, CASE complexes, window functions, expressions régulières, agrégations délicates etc..