Les tests unitaires sont un outil pour documenter et enforcer le comportement aux cas limites. Trois catégories reviennent régulièrement : la gestion des nulls, les tables amont vides et les limites de dates.
Gestion des nulls
La sémantique des nulls en SQL est notoirement déroutante. NULL + 100 = NULL. NULL = NULL n’est pas TRUE. SUM() ignore les nulls, mais AVG() ne compte pas les lignes nulles dans le dénominateur. Ces comportements entraînent des bugs subtils qui ne se manifestent que lorsque des valeurs nulles apparaissent en production.
Testez que vos agrégations gèrent correctement les nulls :
unit_tests: - name: test_int_customers_revenue_null_handling model: int__customers_revenue description: "Les valeurs de commande nulles doivent être traitées comme zéro dans la somme" given: - input: ref('base__shopify__orders') rows: - {customer_id: 1, order_value: 100} - {customer_id: 1, order_value: null} - {customer_id: 1, order_value: 50} - {customer_id: 2, order_value: null} # Tous nulls expect: rows: - {customer_id: 1, total_revenue: 150} - {customer_id: 2, total_revenue: 0}Ce test clarifie le comportement attendu pour deux scénarios :
-
Client 1 a trois commandes : 100, null, 50. Le
SUM()de SQL ignore le null et retourne 150. Mais votre modèle utilise-t-ilCOALESCE(order_value, 0)avant de sommer, ou s’appuie-t-il sur le comportement d’ignorance des nulls deSUM()? Le résultat est le même ici, mais la distinction est importante si vous changez ensuite d’agrégation. -
Client 2 n’a que des commandes nulles.
SUM()sur des lignes toutes nulles retourne NULL, pas 0. Si votre modèle doit retourner 0 pour les clients sans commandes valides, vous avez besoin deCOALESCE(SUM(order_value), 0). La valeur attendue documente le comportement que vous avez choisi.
Le commentaire « Ou null, selon votre logique » est délibéré. Le test ne prescrit pas le bon comportement — il documente et enforce ce que votre modèle fait. Si une modification future inverse accidentellement null-vers-zéro ou zéro-vers-null, le test le détecte.
Tables vides
Que se passe-t-il lorsqu’une table amont est vide ? Peut-être qu’il s’agit d’un nouveau déploiement sans données historiques. Peut-être qu’un système source a échoué et n’a livré aucun enregistrement. Votre modèle doit gérer cela gracieusement — soit en retournant zéro ligne, soit en retournant une ligne avec des valeurs par défaut.
Le défi : format: dict avec rows: [] ne fonctionne pas dans les tests unitaires dbt. dbt ne peut pas inférer les types de colonnes depuis une liste vide. La solution est format: sql avec une clause WHERE false :
unit_tests: - name: test_mrt_finance_daily_revenue_empty_orders model: mrt__finance__daily_revenue description: "Le modèle doit retourner zéro revenu pour les jours sans commandes" given: - input: ref('base__shopify__orders') format: sql rows: | select cast(null as string) as order_id, cast(null as date) as order_date, cast(null as float64) as order_value where false - input: ref('int__dates') rows: - {date_key: "2024-06-01"} expect: rows: - {date_key: "2024-06-01", daily_revenue: 0, order_count: 0}Le format SQL utilise un casting explicite (cast(null as string)) pour définir le schéma des colonnes, puis WHERE false garantit que zéro ligne est retournée. Cela donne à dbt les informations de type dont il a besoin sans fournir de données.
Le test vérifie que lorsque les commandes sont vides mais que les dates existent, le modèle produit tout de même une sortie avec zéro revenu — plutôt que de ne rien retourner ou de lancer une erreur. C’est particulièrement important pour les modèles de reporting qui doivent afficher toutes les dates même en l’absence d’activité.
Cette technique format: sql avec WHERE false est utile au-delà du test de tables vides. Chaque fois que vous devez fournir des colonnes typées que le format rows: standard gère mal (types imbriqués complexes, types BigQuery spécifiques comme STRUCT ou ARRAY), le format SQL vous donne un contrôle total.
Limites de dates
La logique de dates est un champ de mines. Années fiscales qui ne s’alignent pas avec l’année calendaire. Années bissextiles. Calculs de numéros de semaine qui varient selon les pays. Fuseaux horaires qui changent lors des transitions de l’heure d’été. Ces cas limites causent de vrais bugs en production.
Un modèle de calendrier fiscal est un candidat parfait pour les tests de limites :
unit_tests: - name: test_int_fiscal_calendar_boundaries model: int__fiscal_calendar description: "L'année fiscale doit gérer correctement la fin d'année (AF commence le 1er avril)" given: - input: ref('int__dates') rows: - {calendar_date: "2024-03-31"} # Dernier jour de l'AF2024 - {calendar_date: "2024-04-01"} # Premier jour de l'AF2025 - {calendar_date: "2024-02-29"} # Année bissextile - {calendar_date: "2024-12-31"} # Fin d'année calendaire expect: rows: - {calendar_date: "2024-03-31", fiscal_year: 2024, fiscal_quarter: 4} - {calendar_date: "2024-04-01", fiscal_year: 2025, fiscal_quarter: 1} - {calendar_date: "2024-02-29", fiscal_year: 2024, fiscal_quarter: 4} - {calendar_date: "2024-12-31", fiscal_year: 2025, fiscal_quarter: 3}Ce test suppose une année fiscale commençant le 1er avril. Chaque ligne teste un cas limite spécifique :
- 31 mars 2024 : Dernier jour du T4 de l’AF2024. L’année fiscale se termine ici.
- 1er avril 2024 : Premier jour du T1 de l’AF2025. Un jour plus tard, année fiscale et trimestre différents.
- 29 février 2024 : Date de l’année bissextile. Vérifie que le modèle ne plante pas au 29 février.
- 31 décembre 2024 : Fin d’année calendaire, mais en milieu d’année fiscale (T3 de l’AF2025). Si quelqu’un code en dur une hypothèse sur l’année calendaire, ce test échoue.
La même approche de test aux limites tirée de Tester la logique CASE WHEN dans dbt s’applique ici : testez les points de transition, pas le milieu de chaque période. Une date en plein milieu de juillet est sans intérêt — elle est sans ambiguïté au T2 dans une année fiscale commençant le 1er avril. Ce sont les dates aux limites qui posent problème.
Stratégie générale des cas limites
Pour tout modèle, trois questions valent la peine d’être vérifiées :
-
Que se passe-t-il lorsque les valeurs d’entrée sont nulles ? Les nulls se propagent dans les calculs et produisent des résultats inattendus en aval si ils ne sont pas gérés explicitement.
-
Que se passe-t-il lorsqu’une table d’entrée est vide ? Si le modèle effectue des jointures ou des agrégations sur une table vide, retourne-t-il zéro ligne, des lignes par défaut ou une erreur ?
-
Que se passe-t-il aux valeurs limites ? Dates aux transitions de période, nombres aux seuils, chaînes aux limites de longueur.
Si les réponses sont connues avec certitude, aucun test n’est nécessaire. Si le comportement est incertain, écrivez le test.