Quand on veut écrire des tests unitaires avec l’IA, obtenir des tests est la partie facile. Demandez à n’importe quel agent de code « des tests pour ce module » et vous aurez quarante tests verts en quelques minutes. Le plus dur, c’est d’obtenir des tests qui échouent quand le code est faux. Un agent qui lit une fonction puis écrit des assertions sur ce qu’elle renvoie a surtout réécrit la fonction une seconde fois.
Ce guide explique comment tirer des tests utiles d’agents de code comme Claude Code, Codex ou Gemini CLI : quoi leur demander, comment repérer les tests qui ne prouvent rien, comment les mesurer avec la couverture et les tests de mutation, et comment répartir le travail pour que l’agent qui écrit le code ne puisse pas plier les tests en douce.
Pourquoi les tests écrits par l’IA tournent mal
Les agents sont très bons sur la partie mécanique des tests : fixtures, factories, mocks, cas paramétrés, squelette d’un nouveau fichier de test. Là où ils dérapent, c’est sur ce qui donne sa valeur à un test : la valeur attendue.
Quand un agent écrit un test, il a généralement l’implémentation ouverte dans son contexte. Le réflexe naturel est de lire le code, de calculer ce qu’il renvoie et d’affirmer exactement cela. Si le code contient un bug, le test le fige. Il est vert, il couvre les lignes, et il resterait vert si vous ne corrigiez jamais le bug.
Cela prend quelques formes classiques :
- Logique en miroir. Le test calcule la valeur attendue avec la même formule que le code :
expect(price(100, 0.2)).toBe(100 - 100 * 0.2). Si la formule est fausse, les deux le sont ensemble. - Mocker le sujet. Ce qu’on teste est mocké, donc le test vérifie le mock.
- Assertions sur l’appel seul.
expect(sendEmail).toHaveBeenCalled()sans rien vérifier de ce qui a été envoyé, ni à qui. - Tout en snapshot. Un snapshot énorme que personne ne lit, mis à jour avec
-uà chaque échec. - Des assertions qui ne peuvent pas échouer.
expect(result).toBeDefined()sur une fonction qui renvoie toujours un objet. - Modifier le test pour qu’il passe. Le code échoue à un test, et l’agent « corrige » la valeur attendue au lieu du code.
Rien de tout cela n’est propre à l’IA. Les humains en écrivent aussi. Les agents en produisent simplement plus vite et en plus grande quantité, il vous faut donc un processus qui les attrape.
Partir du comportement, pas du code
L’amélioration la plus nette consiste à dire à l’agent ce que le code est censé faire, et à garder les valeurs attendues hors de portée de l’implémentation.
Un prompt faible :
Écris des tests unitaires pour
src/billing/discount.ts.
Un meilleur :
Écris des tests unitaires pour
applyDiscountdanssrc/billing/discount.ts. Règles : une remise en pourcentage ne fait jamais passer le total sous zéro ; les remises ne se cumulent pas, la plus grande l’emporte ; un code expiré est ignoré, ce n’est pas une erreur ; les montants sont arrondis à 2 décimales, au demi supérieur. Écris les valeurs attendues à la main à partir de ces règles, sans exécuter ni lire l’implémentation. Si une règle semble contredire le code actuel, écris quand même le test et dis-moi quels tests échouent.
Trois choses ont changé. Les règles viennent de vous, d’un ticket, d’une spécification ou de la doc. Les valeurs attendues sont des littéraux tirés des règles. Et un test qui échoue est permis : c’est peut-être un vrai bug, et c’est précisément à cela que servent les tests.
Quelques habitudes de plus aident :
- Demandez d’abord la liste des cas. « Liste les cas que tu comptes tester, un par ligne, avant d’écrire du code. » Vous pouvez ajouter les cas limites oubliés (entrée vide, unicode, fuseaux horaires, la limite à exactement zéro) avant que le moindre code existe.
- Nommez les limites. Les agents trouvent bien les cas limites quand vous désignez la dimension : « des entrées pile sur, juste en dessous et juste au-dessus de chaque seuil ».
- Dites ce qu’il ne faut pas mocker. « Ne mocke rien à l’intérieur de
src/billing; mocke seulement le client HTTP. » Le sur-mocking est à l’origine de beaucoup de tests tautologiques. - Un comportement par test. Des tests au nom clair comme
ignores_expired_codesse relisent mieux qu’un seul test avec douze assertions.
Tests unitaires, d’intégration et de bout en bout
Le même agent peut écrire les trois, mais les consignes diffèrent.
Tests unitaires
Rapides, isolés, nombreux. C’est là que les agents font gagner le plus de temps. Montrez à l’agent votre style de test existant (« suis tests/billing/tax.test.ts ») pour ne pas récupérer une nouvelle bibliothèque d’helpers à chaque fois. Mettez la commande de test dans votre CLAUDE.md ou AGENTS.md pour que l’agent lance les tests de lui-même ; voir comment écrire CLAUDE.md et AGENTS.md.
Tests d’intégration
Ils touchent une vraie base de données, une file ou le système de fichiers. Les agents ont tendance à sortir les mocks ici, ce qui retransforme le test d’intégration en test unitaire. Soyez explicite : « utilise la base de test de docker-compose.test.yml ; ne mocke pas la couche repository. » Demandez aussi le nettoyage, pour que les tests ne dépendent pas des restes des autres.
Tests de bout en bout
Pour les tests navigateur, un agent peut écrire des specs Playwright ou Cypress à partir d’un parcours utilisateur que vous décrivez. Le point faible, ce sont les sélecteurs : demandez des sélecteurs par rôle ou par libellé (getByRole('button', { name: 'Pay' })) plutôt que des chemins CSS qui cassent au prochain redesign. L’article sur les tests navigateur avec un agent IA explique comment laisser un agent piloter un vrai navigateur pour vérifier son propre travail.
Des tests de caractérisation pour du code que vous ne comprenez pas encore
Parfois, il n’y a pas de spécification. Le code est ancien, son auteur est parti et personne ne sait exactement ce qu’il fait. L’objectif s’inverse alors : vous voulez des tests qui enregistrent ce que fait le code aujourd’hui, bugs compris, pour remarquer quand cela change.
On les appelle tests de caractérisation, et c’est l’un des cas où laisser l’agent lire l’implémentation est le bon choix. Le prompt change en conséquence :
Écris des tests de caractérisation pour
legacy/invoice_export.py. Enregistre le comportement actuel, y compris ce qui ressemble à un bug ; ne corrige rien. Couvre chaque branche que tu trouves. Marque tout comportement qui semble faux d’un commentaire# SUSPICIOUS:qui explique pourquoi.
Les notes SUSPICIOUS deviennent votre liste de bugs pour plus tard. Les tests de type snapshot ou approval conviennent ici, tant qu’ils restent assez petits pour être lus. C’est la première étape de tout refactoring sûr, que détaille le guide refactoriser du code legacy avec l’IA.
La couverture pour trouver les trous, pas pour noter les tests
La couverture est une bonne carte et une mauvaise note. Servez-vous-en pour dire à l’agent où regarder :
| Stack | Commande |
|---|---|
| Vitest | npx vitest run --coverage |
| Jest | npx jest --coverage |
| pytest (avec pytest-cov) | pytest --cov=myapp --cov-report=term-missing |
| Go | go test -coverprofile=cover.out ./... puis go tool cover -func=cover.out |
Vitest a besoin d’un fournisseur de couverture installé, comme @vitest/coverage-v8. Le rapport term-missing de pytest liste les numéros exacts des lignes non couvertes, pratique à coller tel quel dans un prompt.
Une bonne boucle :
- Lancez la couverture et choisissez les modules qui comptent : facturation, authentification, migrations de données, pas le code généré.
- Donnez à l’agent les lignes non couvertes et demandez-lui quel comportement elles représentent.
- Faites-lui écrire des tests pour ce comportement, avec des valeurs attendues tirées des règles.
- Relancez la couverture pour confirmer que ces lignes sont désormais exercées.
Ce qu’il ne faut pas faire, c’est dire « monte la couverture à 90 % ». Un agent à qui l’on donne un chiffre atteint le chiffre, et le moyen le moins cher d’y arriver, ce sont des tests qui exécutent des lignes sans vérifier grand-chose.
Tests de mutation : les tests vérifient-ils vraiment quelque chose ?
Les tests de mutation répondent à la question que la couverture laisse ouverte. Un outil apporte de petites modifications à votre code, comme changer > en >=, remplacer + par - ou renvoyer une valeur vide, et lance vos tests contre chacune. Si les tests passent encore, le mutant a « survécu », et vous avez trouvé un endroit où les tests ne vérifient pas vraiment le comportement.
Outils courants :
- JavaScript et TypeScript : StrykerJS. Installez-le avec
npm init stryker@latest, puis lanceznpx stryker run. - Python : mutmut. Lancez
mutmut run, puismutmut browsepour parcourir les mutants survivants. Les versions récentes ont besoin de fork, donc sous Windows il tourne dans WSL. - Java : PIT, par exemple
mvn test-compile org.pitest:pitest-maven:mutationCoverage. - Rust : cargo-mutants, installé avec
cargo install cargo-mutantset lancé aveccargo mutants.
Sur une grosse base de code, les runs de mutation sont lents : limitez-les au module sur lequel vous travaillez. Puis confiez les survivants à l’agent :
Ces mutants ont survécu dans
discount.ts: (liste). Pour chacun, écris un test qui échoue sur le mutant et passe sur l’original. Ne modifie pasdiscount.ts.
C’est l’un des meilleurs usages d’un agent en matière de tests. La tâche est précise, le critère de réussite est mécanique, et le résultat est un test dont on a la preuve qu’il attrape une vraie catégorie de bug.
Un agent écrit les tests, un autre écrit le code
Le moyen le plus fiable d’empêcher un agent de plier les tests est de s’assurer que l’agent qui écrit le code ne peut pas y toucher.
Le dispositif est simple :
- Agent de test. Il reçoit la spécification et écrit les tests dans
tests/(ou là où vivent vos tests). Il ne voit ni n’écrit l’implémentation. Ses tests doivent d’abord échouer, puisque la fonctionnalité n’existe pas encore. - Agent implémenteur. Il reçoit la même spécification plus les tests en échec, et écrit le code jusqu’à ce qu’ils passent. Il ne peut rien modifier dans
tests/. - Les désaccords remontent. Si l’implémenteur pense qu’un test est faux, il le demande à l’agent de test, ou à vous, au lieu de le modifier.
C’est du développement piloté par les tests réparti sur deux agents, et cela marche aussi entre fournisseurs. Certaines équipes aiment confier les tests à un modèle et l’implémentation à un autre, pour que les deux ne partagent pas les mêmes angles morts.
Faire tourner deux agents sur la même fonctionnalité demande un peu d’isolation pour qu’ils ne se marchent pas dessus. Un git worktree par agent est la réponse habituelle ; voir git worktrees pour agents parallèles. Pour ce découpage précis, vous pouvez aussi garder les deux dans un seul worktree, puisqu’ils écrivent dans des dossiers différents.
Relire des tests écrits par l’IA
Relisez les tests au moins aussi attentivement que le code. Une courte check-list :
- Les valeurs attendues viennent-elles des règles ? Cherchez des littéraux, pas des formules copiées du code.
- Ce test échouerait-il si le code était faux ? Prenez un test et cassez le code à la main. S’il reste vert, le test est décoratif.
- Quelque chose d’important est-il mocké ? Des mocks à la frontière, c’est bien ; des mocks du code testé, non.
- Un test existant a-t-il changé ? Sur une branche de fonctionnalité, des valeurs attendues modifiées dans d’anciens tests méritent une question.
git diff --stat -- tests/le montre d’un coup d’œil. - Les noms sont-ils lisibles ? Le nom d’un test en échec doit dire ce qui a cassé.
- Est-il instable ? Méfiez-vous de l’heure réelle, des données aléatoires, des appels réseau et de l’ordre des tests. Demandez à l’agent de lancer les nouveaux tests plusieurs fois.
La revue de code automatique aide aussi. Un second modèle qui lit le diff repère souvent un test qui n’affirme rien. L’article sur la revue de code par IA explique comment cela fonctionne.
Erreurs courantes
- « Écris des tests pour ce fichier », sans rien d’autre. Vous obtenez l’implémentation, reformulée.
- Courir après un chiffre de couverture. Vous obtenez des lignes exécutées, pas un comportement vérifié.
- Laisser l’implémenteur modifier les tests. Le chemin le plus court vers le vert, c’est une assertion changée.
- Mettre à jour les snapshots sans les lire.
-utransforme une régression en nouvelle sortie attendue. - Tester du code que vous allez supprimer. Testez le comportement que vous garderez.
- Laisser des tests instables. Un agent qui voit échouer un test instable « corrigera » volontiers quelque chose qui n’était pas cassé. C’est vrai aussi pour corriger les échecs de CI avec l’IA.
Le faire dans AgentVera
AgentVera fait tourner des CLI d’agents de code côte à côte par projet, donc le duo agent de test et implémenteur se met en place en quelques minutes.
- Deux agents, une fonctionnalité. Ouvrez un agent de test et un implémenteur dans le même projet, sur la même CLI ou sur deux différentes, par exemple Claude Code pour les tests et Codex pour le code. Placez-les dans un worktree séparé depuis le panneau des worktrees si vous voulez garder le travail hors de votre dossier principal.
- La propriété des fichiers tient l’implémenteur à l’écart des tests. Dans la boîte de dialogue de l’agent de test, ajoutez
tests/**à ses zones possédées. Tant qu’il tourne, les autres agents ne peuvent pas modifier ces fichiers. Pour les agents Claude Code, la modification est refusée avant d’avoir lieu par un hook PreToolUse ; Codex et les autres CLI reçoivent la règle dans leurs instructions. Les tentatives refusées apparaissent dans la chronologie de l’équipe. - La messagerie entre agents pour les désaccords. Quand l’implémenteur pense qu’un test est faux, il peut écrire directement à l’agent de test et attendre sa réponse, au lieu de modifier le test.
- Verify contrôle le résultat. Écrivez dans votre message ce que « terminé » veut dire, par exemple « tous les tests de
tests/billingpassent et aucun fichier de test n’a changé ». Verify en tire des critères et, à la fin du tour de l’agent, un vérificateur distinct lance les tests et marque chaque critère avec une preuve. Le vérificateur peut lire le dépôt et lancer des commandes de test, mais il ne peut pas modifier de fichiers. - La revue de code lit les tests. La revue de code automatique examine les changements de chaque tour avec le modèle Claude de votre choix et liste ses constats avec une sévérité et un lien fichier:ligne, un bon second regard sur les assertions faibles.
- Des checkpoints pour les mauvais tours. Chaque tour est enregistré dans un snapshot : si un agent réécrit la moitié de la suite de tests dans une direction qui ne vous plaît pas, vous annulez ce tour et recommencez. Voir comment annuler les changements de Claude Code.
Pour conclure
L’IA rend l’écriture de tests bon marché, ce qui déplace le travail : on ne tape plus les tests, on les juge. Donnez à l’agent un comportement plutôt que du code, gardez les valeurs attendues hors de portée de l’implémentation, prenez la couverture comme carte et les tests de mutation comme note, et ne laissez pas l’agent qui écrit le code décider aussi de ce que « passer » veut dire. Faites cela et le gain de vitesse est réel : une suite de tests qui grandit vite et qui échoue toujours quand quelque chose casse.
Pour aller plus loin :
- Verify
- Propriété des fichiers
- Messagerie entre agents
- Revue de code
- Checkpoints et replay
- Agents de code en parallèle
Pour essayer AgentVera, commencez par la page d’accueil.