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 :
# Construire les versions schéma-uniquement des modèles amontdbt run --select +model_with_unit_tests --empty
# Puis exécuter les tests unitairesdbt test --select test_type:unitLe 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 citygiven: - input: ref('base__crm__customers') rows: - {customer_id: 1, address: {city: "Paris"}} # Champs manquants !
# Vous devez spécifier tous les champs du STRUCTgiven: - 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 naturellementgiven: - input: ref('base__events') rows: - {event_id: 1, tags: ["click", "mobile"]}
# Mal : le CSV ne peut pas représenter les tableaux proprement# Évitez cette combinaisonPour 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_paramsVerbeux, mais explicite et fiable.
Dépannage des erreurs courantes
Ces erreurs reviennent régulièrement lors de l’écriture de tests unitaires BigQuery :
| Erreur | Cause | Solution |
|---|---|---|
| ”Not able to get columns for unit test” | Les modèles parents n’existent pas dans la base de données | Exécutez d’abord dbt run --select +test_type:unit --empty |
| ”node not found” pendant la compilation | Un ref() dans votre modèle n’est pas mocké dans given | Ajoutez le ref manquant comme entrée, même avec des lignes vides |
| ”check data types” mismatch | Les types de données mock ne correspondent pas à la sortie du modèle | Utilisez le casting explicite : {amount: "10.00::numeric"} ou passez à format: sql |
| Le test de modèle éphémère échoue | dbt ne peut pas requêter les modèles éphémères pour leur schéma | Utilisez format: sql pour les entrées de modèles éphémères (doit inclure TOUTES les colonnes) |
| La comparaison STRUCT échoue | BigQuery ne peut pas comparer les types complexes via EXCEPT | Utilisez 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 :
- Utilisez
--emptypour les builds amont comme décrit ci-dessus. C’est la plus grande économie de coût. - 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.
- 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.
- É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.