ServicesÀ proposNotesContact Me contacter →
EN FR
Note

Contournements BigQuery pour les tests unitaires dbt

Les pièges spécifiques à BigQuery pour les tests unitaires dbt — complétude des STRUCT, comparaisons ARRAY, column_transformations, coûts de slots et solutions aux erreurs courantes.

Planté
dbtbigquerytesting

Les tests unitaires dbt fonctionnent sur tous les entrepôts supportés, mais BigQuery introduit quelques particularités qui vous surprendront si vous n’y êtes pas préparé. La plupart découlent de la gestion des types complexes par BigQuery (STRUCTs et ARRAYs) et de son modèle d’exécution basé sur les slots. Aucune n’est rédhibitoire, mais elles nécessitent des contournements spécifiques.

Comment les tests unitaires s’exécutent sur BigQuery

Lorsque vous exécutez un test unitaire, dbt génère une requête SQL avec des CTEs contenant vos données mockées, puis exécute le SQL de votre modèle contre ces CTEs. La sortie est comparée ligne par ligne à vos résultats attendus.

L’implication importante : les tests unitaires consomment des slots BigQuery. Ils ne sont pas gratuits. Chaque test est une vraie requête exécutée dans votre entrepôt. Pour quelques tests, c’est négligeable, mais si vous avez des centaines de tests unitaires qui s’exécutent en CI, le coût s’accumule.

Vous pouvez minimiser cela en utilisant le flag --empty lors de la construction des modèles parents avant d’exécuter les tests unitaires :

Terminal window
# Construire les versions schéma-uniquement des modèles amont
dbt run --select +model_with_unit_tests --empty
# Puis exécuter les tests unitaires
dbt test --select test_type:unit

Le flag --empty crée des tables avec les schémas corrects mais zéro ligne. C’est suffisant pour la compilation des tests unitaires car ceux-ci utilisent leurs propres données mockées — ils ont juste besoin que les schémas amont existent pour que le SQL compile.

Les champs STRUCT doivent être complets

Contrairement aux colonnes ordinaires où vous pouvez ne spécifier que les champs que votre logique utilise, les champs STRUCT nécessitent que tous les champs imbriqués soient présents dans vos données mock :

# Cela ne fonctionnera pas si address a plus de champs que city
given:
- input: ref('base__crm__customers')
rows:
- {customer_id: 1, address: {city: "Paris"}} # Champs manquants !
# Vous devez spécifier tous les champs du STRUCT
given:
- input: ref('base__crm__customers')
rows:
- {customer_id: 1, address: {street: "123 Rue Example", city: "Paris", postal_code: "75001", country: "FR"}}

C’est une exigence spécifique à BigQuery. Lorsque dbt génère la CTE pour votre STRUCT mocké, il doit construire un littéral STRUCT valide, et BigQuery ne permet pas la construction partielle de STRUCT. Chaque champ a besoin d’une valeur (même si c’est null).

Cela devient fastidieux rapidement pour les STRUCTs profondément imbriqués comme ceux présents dans les données d’événements GA4, où le STRUCT event_params a de nombreux champs. Le conseil pratique : si votre modèle ne lit que event_params.value.string_value, vous devez quand même inclure tous les champs frères dans la définition du STRUCT pour le mock. Utilisez null pour les champs qui ne vous intéressent pas.

Échecs de comparaison STRUCT et ARRAY

BigQuery ne supporte pas les opérations EXCEPT sur les types complexes. Lorsque dbt compare la sortie de votre modèle aux lignes attendues, il utilise des opérations de différence d’ensembles — et celles-ci échouent sur les colonnes contenant des STRUCTs ou des ARRAYs.

La solution est column_transformations, qui vous permet de convertir les types complexes en chaînes comparables avant la comparaison :

unit_tests:
- name: test_event_aggregation
model: int__users_activity_summary
given:
- input: ref('base__ga4__events')
rows:
- {user_id: 1, event_type: "click", properties: {page: "/home", duration: 30}}
expect:
format: dict
rows:
- {user_id: 1, activity_details: {total_events: 1, event_types: ["click"]}}
column_transformations:
activity_details: "to_json_string(##column##)"

Le placeholder ##column## est remplacé par la référence de colonne réelle au moment de l’exécution. to_json_string() est la fonction de référence ici — elle sérialise n’importe quel type complexe en une représentation sous forme de chaîne comparable.

Vous pouvez appliquer column_transformations à plusieurs colonnes :

expect:
rows:
- {user_id: 1, tags: ["premium", "active"], address: {city: "Paris"}}
column_transformations:
tags: "to_json_string(##column##)"
address: "to_json_string(##column##)"

Un piège subtil : la sérialisation JSON doit produire des chaînes identiques pour que la comparaison réussisse. Si votre modèle produit ["active", "premium"] mais que votre sortie attendue a ["premium", "active"], le test échoue car les chaînes JSON diffèrent même si les tableaux contiennent les mêmes éléments. Faites attention à l’ordre.

Colonnes ARRAY en format CSV

N’utilisez pas le format CSV pour les entrées qui contiennent des colonnes ARRAY. Le CSV n’a pas de manière native de représenter les tableaux, et le comportement du parsing est peu fiable. Restez sur le format dict pour les tableaux :

# Bien : le format dict gère les tableaux naturellement
given:
- input: ref('base__events')
rows:
- {event_id: 1, tags: ["click", "mobile"]}
# Mal : le CSV ne peut pas représenter les tableaux proprement
# Évitez cette combinaison

Pour les scénarios complexes impliquant des ARRAYs de STRUCTs (courants dans les données GA4), utilisez le format SQL à la place :

given:
- input: ref('base__ga4__events')
format: sql
rows: |
select
1 as event_id,
[struct('page_location' as key, struct('/home' as string_value, cast(null as int64) as int_value) as value)] as event_params

Verbeux, mais explicite et fiable.

Dépannage des erreurs courantes

Ces erreurs reviennent régulièrement lors de l’écriture de tests unitaires BigQuery :

ErreurCauseSolution
”Not able to get columns for unit test”Les modèles parents n’existent pas dans la base de donnéesExécutez d’abord dbt run --select +test_type:unit --empty
”node not found” pendant la compilationUn ref() dans votre modèle n’est pas mocké dans givenAjoutez le ref manquant comme entrée, même avec des lignes vides
”check data types” mismatchLes types de données mock ne correspondent pas à la sortie du modèleUtilisez le casting explicite : {amount: "10.00::numeric"} ou passez à format: sql
Le test de modèle éphémère échouedbt ne peut pas requêter les modèles éphémères pour leur schémaUtilisez format: sql pour les entrées de modèles éphémères (doit inclure TOUTES les colonnes)
La comparaison STRUCT échoueBigQuery ne peut pas comparer les types complexes via EXCEPTUtilisez column_transformations avec to_json_string()

L’erreur “Not able to get columns” est la plus courante pour les équipes qui découvrent les tests unitaires. Ce n’est pas un bug — dbt a besoin que les schémas de tables amont existent dans la base de données pour pouvoir compiler le SQL de test unitaire. Le flag --empty crée ces schémas sans traiter aucune donnée.

L’erreur de incompatibilité de types est particulièrement insidieuse. Si votre modèle produit un FLOAT64 mais que vos données mock fournissent un INT64, la comparaison échoue même si les valeurs sont numériquement égales. En cas de doute, utilisez le format SQL pour le bloc expect pour pouvoir caster explicitement les types.

Optimisation des coûts

Pour les équipes exécutant de nombreux tests unitaires contre BigQuery, quelques pratiques maintiennent les coûts gérables :

  1. Utilisez --empty pour les builds amont comme décrit ci-dessus. C’est la plus grande économie de coût.
  2. Gardez les jeux de données mock petits. Quatre à six lignes par entrée suffisent généralement pour tester les cas limites. Vous testez la logique, pas la charge.
  3. Exécutez les tests unitaires sur une réservation séparée et plus petite si vous utilisez les éditions BigQuery. Les tests unitaires sont des requêtes légères qui n’ont pas besoin de la même capacité en slots que la production.
  4. Étiquetez les tests unitaires par priorité pour pouvoir exécuter les tests critiques fréquemment et les moins critiques selon un calendrier.

La section workflow CI/CD couvre le pattern complet pour minimiser les coûts BigQuery dans les pipelines automatisés.