Le problème
Les tests natifs de dbt (unique, not_null, relationships, tests génériques custom) couvrent très bien la validation de contraintes sur un modèle isolé. Mais dès qu'il s'agit de vérifier une logique métier qui traverse plusieurs modèles (par exemple que la somme des lignes d'une table de faits agrégée correspond à la source, ou qu'un changement de schéma amont ne casse pas silencieusement un calcul en aval), les tests dbt YAML deviennent vite illisibles ou insuffisants. La question qu'on s'est posée : faut-il pousser dbt plus loin, ou basculer une partie de la validation vers une suite pytest externe ?
Option 1 : tests dbt natifs
Les tests génériques (schema tests) suffisent pour la majorité des cas de validation de données :
# models/marts/schema.yml
models:
- name: fct_orders
columns:
- name: order_id
tests:
- unique
- not_null
- name: customer_id
tests:
- relationships:
to: ref('dim_customers')
field: customer_idPour des règles plus spécifiques, un test singulier (une requête SQL qui doit retourner zéro ligne) reste dans le même langage que les modèles, versionné au même endroit :
-- tests/assert_order_totals_positive.sql
select order_id, total_amount
from {{ ref('fct_orders') }}
where total_amount < 0Avantages : colocalisé avec le modèle, exécuté par dbt test dans le même run CI que le build, zéro dépendance supplémentaire, lisible par n'importe qui qui connaît déjà dbt.
Limites : difficile d'exprimer des assertions qui comparent plusieurs runs dans le temps, qui font des appels à des APIs externes, ou qui nécessitent une logique Python (parsing, calculs statistiques). Le passage à l'échelle sur des règles métier complexes rend le YAML/SQL vite difficile à maintenir.
Option 2 : pytest en complément
On a mis en place une suite pytest légère qui tourne après dbt build en CI, avec un client de connexion à l'entrepôt (on utilise le dbtRunner programmatique de dbt-core pour rester dans le même contexte de profil/target) :
# tests/test_revenue_consistency.py
import pytest
from tests.warehouse_client import query
def test_daily_revenue_matches_source():
result = query("""
select
(select sum(total_amount) from marts.fct_orders) as mart_total,
(select sum(amount) from staging.stg_raw_payments) as source_total
""")
row = result[0]
# Tolérance pour les remboursements comptabilisés différemment en amont
assert abs(row["mart_total"] - row["source_total"]) < row["source_total"] * 0.001
@pytest.mark.parametrize("env", ["staging", "production"])
def test_no_orphan_customers(env):
result = query(f"""
select count(*) as orphans
from {env}.fct_orders o
left join {env}.dim_customers c using (customer_id)
where c.customer_id is null
""")
assert result[0]["orphans"] == 0Avantages : logique Python complète disponible (paramétrage, comparaison entre environnements, appels externes), intégration naturelle avec le reste de la CI existante (souvent déjà pytest pour le code applicatif), meilleure lisibilité pour des assertions complexes avec tolérances ou calculs multi-étapes.
Limites : plus loin des modèles dans le repo, duplication possible avec certains tests dbt basiques si on n'est pas rigoureux sur la répartition, courbe d'apprentissage pour l'équipe analytics qui ne connaît pas forcément pytest aussi bien que le SQL/YAML dbt.
Comparatif
| Critère | Tests dbt | pytest |
|---|---|---|
| Rapidité de mise en place | Très rapide pour des règles simples | Setup initial plus lourd (client warehouse, fixtures) |
| Couverture de cas complexes | Limitée (SQL pur) | Large (logique Python, comparaisons multi-sources) |
| Intégration CI | Native (dbt test) |
Nécessite une étape CI dédiée après le build |
| Lisibilité pour l'équipe data | Élevée | Dépend de la familiarité avec pytest |
Ce qu'on utilise en prod et pourquoi
On garde les tests dbt natifs pour tout ce qui est contrainte de données locale à un modèle : unicité, nullité, intégrité référentielle, ranges de valeurs via dbt_utils.accepted_range. C'est rapide à écrire, ça vit avec le modèle, et ça couvre 80 % des besoins réels de qualité de données.
La suite pytest est réservée aux règles de cohérence transverses (réconciliation source/mart, comparaison inter-environnements, règles métier avec tolérance ou logique conditionnelle) qui seraient soit impossibles soit illisibles en SQL pur. Elle tourne dans un job CI séparé, après le dbt build, avec le même warehouse de test.
Recommandation
Ne pas voir ça comme un choix binaire. Pour une équipe qui démarre, les tests dbt natifs suffisent largement au début et coûtent très peu à mettre en place. La bascule vers pytest devient pertinente au moment où on se retrouve à écrire des tests singuliers SQL de plus en plus tordus pour exprimer une logique qui serait triviale en Python. C'est le signal qu'il est temps d'ajouter une suite pytest à côté, pas de remplacer l'existant.