02 - Ecrire et corriger des tests
Un test genere en 10 secondes qui ne teste rien ne vaut rien ; ce fichier montre comment obtenir des tests qui attrapent de vrais bugs.
Temps de lecture : 14 min | Niveau : Intermediaire
Ce que tu sauras faire apres
- Faire generer des tests unitaires qui suivent les conventions deja presentes dans ton repo.
- Obtenir la liste des cas limites auxquels tu n avais pas pense.
- Ecrire un test de regression qui verrouille un bug corrige.
- Tester un pipeline de donnees (pandas, dbt, Airflow), pas seulement une fonction pure.
- Reconnaitre et bloquer le test bidon qui passe sans rien verifier.
Pourquoi les tests sont le sujet central de toute la section
La doc Claude Code le dit sans detour :
"Give Claude a check it can run: tests, a build, a screenshot to compare. It's the difference between a session you watch and one you walk away from." -- Best practices
Sans test, tu es toi-meme la boucle de verification. Avec test, le modele boucle tout seul : il code, il lance, il lit le resultat, il corrige.
C est pour ca que ce fichier vient avant le refactoring (fichier 03) et avant la revue (fichier 05).
Le reflexe de base : donner les criteres AVANT
La doc officielle montre la difference sous forme de tableau. Version francaise :
| Avant | Apres |
|---|---|
| "implement a function that validates email addresses" | "write a validateEmail function. example test cases: user@example.com is true, invalid is false, user@.com is false. run the tests after implementing" |
Deux differences : des exemples concrets d entree/sortie, et l ordre explicite de lancer les tests.
Template - Demander du code avec ses criteres
Ecris [NOM_DE_LA_FONCTION] dans @[CHEMIN/FICHIER.py].
Comportement attendu, cas par cas :
- entree [VALEUR_1] -> sortie [RESULTAT_1]
- entree [VALEUR_2] -> sortie [RESULTAT_2]
- entree [VALEUR_INVALIDE] -> leve [TYPE_EXCEPTION]
Ecris aussi les tests correspondants dans @[CHEMIN/TEST.py].
Puis lance [pytest -q CHEMIN/TEST.py] et corrige jusqu a ce que tout passe.
Montre-moi la sortie de la commande.La derniere phrase compte : la doc recommande de "Have Claude show evidence rather than asserting success: the test output, the command it ran and what it returned."
Generer des tests sur du code existant
AVANT / APRES
Mauvais prompt :
add tests for foo.pyPrompt ameliore (exemple exact de la doc Claude Code) :
write a test for foo.py covering the edge case where the user is logged out.
avoid mocks.Pourquoi c est mieux : un fichier precis, un scenario precis, une contrainte de style. Sans ca, tu obtiens trois tests triviaux sur le chemin nominal.
La doc ajoute que le modele s aligne sur l existant : "Claude examines your existing test files to match the style, frameworks, and assertion patterns already in use." Donc pointe-lui un test que tu trouves bien ecrit : c est du few-shot prompting, c est-a-dire "montrer un exemple plutot que decrire".
Template - Tests sur code existant
Fichier a tester : @[CHEMIN/SOURCE.py]
Fonction ciblee : [NOM_FONCTION]
Exemple de test bien ecrit dans ce repo, suis exactement ce style :
@[CHEMIN/TEST_EXISTANT.py]
Ecris des tests pytest qui couvrent :
1. le cas nominal
2. [CAS_LIMITE_QUE_TU_CONNAIS]
3. le comportement en cas d entree invalide
Contraintes :
- pas de mock sauf pour [CE_QUI_SORT_DU_PROCESSUS : appel reseau, S3, base]
- un assert par comportement, pas d assert fourre-tout
- nomme chaque test test_<action>_<condition>_<resultat_attendu>
Puis lance `pytest -q [CHEMIN/TEST.py]` et montre-moi la sortie.Trouver les cas limites auxquels tu n as pas pense
C est le vrai gain. La doc Claude Code :
"ask Claude to identify edge cases you might have missed. Claude can analyze your code paths and suggest tests for error conditions, boundary values, and unexpected inputs that are easy to overlook."
Template - Inventaire des cas limites (sans ecrire de code)
Fichier : @[CHEMIN/SOURCE.py]
N ecris AUCUN test pour l instant. Donne-moi seulement la liste des cas
limites de [NOM_FONCTION], classee en 4 familles :
1. VALEURS AUX BORNES (0, 1, vide, tres grand, negatif)
2. TYPES INATTENDUS (None, string au lieu de nombre, NaN, date invalide)
3. ETATS EXTERNES (fichier absent, table vide, API qui repond 500, timeout)
4. CONCURRENCE ET REJEU (appel deux fois, execution partielle, doublons)
Pour chaque cas : une ligne, avec l entree concrete et le comportement
attendu. Marque d un [?] les cas ou tu n es pas sur du comportement voulu.Tu lis la liste, tu coches ce qui compte, puis tu enchaines :
Ecris les tests pytest pour les cas 1, 3 et 7 de ta liste. Les autres, on
les laisse de cote.Cette separation en deux temps ("d abord lister, ensuite coder") est la technique break the task down documentee par Microsoft : "Large language models (LLMs) often perform better if the task is broken down into smaller steps."
Le test de regression apres un bug
Regle a graver : un bug corrige sans test revient.
La doc Claude Code propose l ordre inverse de l intuition : "write a failing test that reproduces the issue, then fix it". On ecrit le test AVANT le correctif, pour prouver qu il attrape bien le bug.
Template - Test de regression
Bug constate : [DECRIS_LE_SYMPTOME_EN_UNE_PHRASE]
Fichier suspect : @[CHEMIN/SOURCE.py]
Reproduction : [COMMANDE_OU_DONNEE_QUI_DECLENCHE_LE_BUG]
Etape 1 : ecris un test pytest qui ECHOUE aujourd hui a cause de ce bug.
Lance-le et montre-moi qu il est bien rouge.
Etape 2 : seulement apres, corrige le code source.
Etape 3 : relance `pytest -q` en entier et montre-moi la sortie. Aucun autre
test ne doit casser.
Nomme le test test_regression_[MOT_CLE_DU_BUG].Les fixtures pytest : ne pas repeter la mise en place
Une fixture est un bout de code qui prepare ce dont le test a besoin (une connexion, un DataFrame d exemple, un fichier temporaire) et qui nettoie apres.
Cinq points a connaitre, d apres la documentation pytest :
| Element | A quoi ca sert |
|---|---|
@pytest.fixture | Le decorateur qui transforme une fonction en fixture. |
scope= | Combien de temps la fixture vit. Valeurs : function (par defaut), class, module, package, session. |
conftest.py | Les fixtures qui y sont declarees sont disponibles dans tous les tests du dossier, sans import. |
yield | A la place de return. Ce qui suit le yield s execute apres le test : c est le nettoyage. |
autouse=True | La fixture s applique meme si le test ne la demande pas. |
Exemple de fixture avec nettoyage, tire de la doc pytest :
@pytest.fixture
def sending_user(mail_admin):
user = mail_admin.create_user()
yield user
mail_admin.delete_user(user)Template - Extraire des fixtures
Fichier de tests : @[CHEMIN/TEST.py]
Ces tests repetent la meme preparation. Refactorise-les :
1. extrais la preparation commune en fixtures pytest
2. mets dans conftest.py celles qui servent a plusieurs fichiers
3. utilise yield pour le nettoyage quand il y a une ressource a liberer
4. choisis le scope minimal qui suffit (function par defaut ; module ou
session seulement si la creation est couteuse)
Ne change AUCUN comportement teste. Lance `pytest -q` avant et apres, et
montre-moi que le nombre de tests passes est identique.Tester plusieurs jeux de donnees : parametrize
Exemple exact de la doc pytest :
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
assert eval(test_input) == expectedLe decorateur donne les noms des deux arguments, puis la liste des couples entree / sortie attendue. La fonction de test est ecrite une fois et pytest la rejoue pour chaque couple.
Applique a la data, c est l outil ideal pour tester une fonction de nettoyage sur toutes les formes de donnees sales que tu rencontres :
import pytest
from src.cleaning import parse_amount
@pytest.mark.parametrize(
"brut,attendu",
[
("1234.56", 1234.56),
("1 234,56", 1234.56),
("$1,234.56", 1234.56),
("", None),
(None, None),
("N/A", None),
],
)
def test_parse_amount(brut, attendu):
assert parse_amount(brut) == attenduTemplate - Parametrize sur des donnees sales
Fonction : @[CHEMIN/SOURCE.py] -> [NOM_FONCTION]
Ecris un unique test pytest avec @pytest.mark.parametrize qui couvre toutes
les formes d entree suivantes :
[COLLE_ICI_5_A_10_VALEURS_REELLES_PRISES_DANS_TES_DONNEES]
Pour chaque valeur, indique la sortie attendue. Si tu n es pas sur de la
sortie attendue pour une valeur, mets-la dans une liste separee "A_TRANCHER"
au lieu de deviner.Tester un pipeline de donnees
Une fonction pure se teste facilement. Un pipeline, moins. Voici les trois niveaux qui marchent.
Niveau 1 - Le DAG se charge-t-il ?
La doc Airflow appelle ca le DAG loader test. Le plus simple :
python dags/mon_dag.pySi ca ne leve pas d erreur, il n y a ni faute de syntaxe, ni import manquant. La doc precise que ce test doit tourner "in an environment that corresponds to your scheduler environment".
Version pytest, exemple exact de la doc Airflow :
import pytest
from airflow.dag_processing.dagbag import DagBag
@pytest.fixture()
def dagbag():
return DagBag()
def test_dag_loaded(dagbag):
dag = dagbag.get_dag(dag_id="hello_world")
assert dagbag.import_errors == {}
assert dag is not None
assert len(dag.tasks) == 1Niveau 2 - L operateur fait-il ce qu on croit ?
La doc Airflow conseille d appeler l operateur directement, sans base de metadonnees :
def test_my_custom_operator_execute():
op = MyCustomOperator(
task_id="my_custom_operator_task", prefix="s3://bucket/some/prefix"
)
assert op.execute(context={}) == "expected return value"Tu n as besoin ni d un DAG, ni d un DAG run, ni de la base de metadonnees : tu instancies l operateur et tu appelles execute() avec les cles de contexte que ton code lit vraiment.
Pour un sensor, la doc dit d appeler poke() et de verifier le booleen renvoye.
Niveau 3 - La donnee produite est-elle correcte ?
C est le self-check : une tache en aval qui verifie le resultat de la tache en amont. La doc Airflow donne l exemple d un S3KeySensor pour verifier qu une partition existe bien apres ecriture.
En SQL, c est une requete de controle. Template :
-- Controles a lancer apres chaque run, doivent tous retourner 0 ligne
-- 1. Doublons sur la cle primaire
SELECT order_id, COUNT(*) AS n
FROM marts.fct_sales
GROUP BY order_id
HAVING COUNT(*) > 1;
-- 2. Valeurs nulles interdites
SELECT COUNT(*) FROM marts.fct_sales WHERE revenue_eur IS NULL;
-- 3. Coherence avec la source (ecart tolere : 0)
SELECT
(SELECT COUNT(*) FROM staging.stg_orders WHERE dt = CURRENT_DATE())
-
(SELECT COUNT(*) FROM marts.fct_sales WHERE dt = CURRENT_DATE())
AS ecart_lignes;Template - Tests de pipeline
Pipeline : @[CHEMIN/DAG_OU_MODELE]
Sortie produite : [TABLE_OU_FICHIER_DE_SORTIE]
Cle primaire attendue : [COLONNE_OU_COMBINAISON]
Colonnes qui ne doivent jamais etre NULL : [LISTE]
Volume attendu par jour : entre [MIN] et [MAX] lignes
Ecris-moi :
1. un test qui verifie que le DAG se charge sans erreur d import
2. un test unitaire de la fonction de transformation, sur un petit DataFrame
ecrit en dur dans le test (pas de lecture de fichier)
3. les requetes SQL de controle qualite a lancer apres chaque run, chacune
devant retourner 0 ligne quand tout va bien
Pour le point 2, construis le DataFrame d entree ET le DataFrame attendu, et
compare avec pandas.testing.assert_frame_equal.Le detail assert_frame_equal evite le piege du test qui verifie seulement len(df) > 0.
Le test bidon : comment l empecher
Le risque connu : le modele fait passer le test au lieu de corriger le code. La doc Anthropic nomme le probleme et donne le texte a coller :
"Do not hard-code values or create solutions that only work for specific test inputs. Instead, implement the actual logic that solves the problem generally. [...] Tests are there to verify correctness, not to define the solution." -- Prompting best practices
Template anti-triche (a coller quand tu demandes un correctif) :
Contraintes non negociables :
- n ecris pas de valeur en dur pour faire passer un test
- ne modifie pas le test pour l adapter au code, sauf si le test est faux ;
dans ce cas, dis-le-moi explicitement avant de le changer
- implemente la logique generale, pas le cas particulier du test
- si la demande est impossible ou si un test est incorrect, dis-le-moi au
lieu de contournerTrois signaux qui doivent t alerter en relisant un test genere :
- l assert porte sur une constante litterale recopiee depuis la sortie observee ;
- le test ne contient aucun
assertsignificatif (juste "ca ne plante pas") ; - un
mockremplace justement la logique qu on voulait tester.
A retenir
- Donne les criteres d acceptation AVANT de demander le code : entree -> sortie, cas par cas.
- Demande d abord la LISTE des cas limites, tranche toi-meme, puis fais ecrire les tests.
- Apres un bug : test rouge d abord, correctif ensuite, suite complete verte a la fin.
- Pointe un test existant que tu trouves bien ecrit : le modele copie ce style.
- Pour un pipeline : DAG qui se charge, transformation testee sur un mini DataFrame, requetes de controle apres run.
- Colle le bloc anti-triche des que tu demandes un correctif.
Sources
- Claude Code - Common workflows
- Claude Code - Best practices
- Anthropic - Prompting best practices
- pytest - How to use fixtures
- pytest - How to parametrize fixtures and test functions
- Apache Airflow - Best Practices (Testing a DAG)
- Microsoft Learn - Prompt engineering techniques
- GitHub Docs - Best practices for using GitHub Copilot