Tests d'intégration avec l'agent
Le test d'intégration comme filet et spec exécutable pour l'agent : vraies dépendances via conteneur jetable, host de test in-memory, le piège du provider de base in-memory, les tests flaky qui font boucler l'agent, et le garde-fou contre le drift silencieux quand l'agent affaiblit un test pour le faire passer.
Le test d’intégration comme spec exécutable et filet
Un test unitaire isole une unité de logique et remplace ses voisins par des doublures. Un test d’intégration fait l’inverse à l’endroit qui compte : il fait travailler ensemble plusieurs composants à travers leurs coutures réelles — une vraie base de données, un vrai pipeline HTTP, un adapter qui parle au monde extérieur. C’est là que se cachent les bugs que l’unitaire ne voit jamais : une requête SQL invalide, une contrainte d’unicité oubliée, un middleware mal ordonné, une sérialisation cassée.
Pour un agent, ce test joue deux rôles à la fois. C’est une spécification exécutable : il décrit sans ambiguïté le comportement attendu aux frontières. Et c’est un filet : tant qu’il est vert, le risque de régression aux coutures est contrôlé. C’est le levier LP7 (Tests) appliqué à l’endroit le plus glissant du système.
Point clé : Le test unitaire vérifie une brique ; le test d’intégration vérifie que les briques tiennent ensemble. Les deux sont complémentaires, pas concurrents.
La boucle fermée : l’agent lance, voit le rouge, corrige
Le test d’intégration devient puissant quand l’agent l’exécute lui-même. C’est le Closed-Loop Prompting : on inscrit la vérification et la correction dans le prompt, et l’agent itère jusqu’à convergence.
Implemente l'adapter de persistance pour OrderRepository.
Puis lance la suite d'integration (dotnet test --filter Category=Integration).
Si un test echoue, lis le message d'erreur, corrige, et relance.
Repete jusqu'a ce que tous les tests passent, max 5 tentatives.
Le mécanisme repose sur deux leverage points empilés. Le test (LP7) donne un verdict binaire et reproductible. La sortie console (LP5) rend ce verdict visible : l’agent lit le rouge, comprend l’échec, agit. Un agent aveugle à la sortie de ses tests ne peut pas se corriger.
Point clé : Un test d’intégration en boucle fermée transforme l’agent en correcteur autonome : observer le rouge, corriger, relancer — sans intervention humaine à chaque tour.
Quoi tester en intégration, quoi tester en unitaire
La pyramide de tests reste valable avec un agent. Le gros de la logique métier se teste vite et à l’isolation : c’est le terrain de la boucle red-green rapide, où chaque tour prend des secondes. On réserve l’intégration aux coutures qui le justifient — la persistance, le pipeline HTTP, les adapters réels — parce que ces tests sont plus lents et plus coûteux à mettre en place.
En architecture hexagonale, le découpage devient net : on teste le domaine à travers ses ports (unitaire, rapide), et l’infrastructure à travers ses adapters (intégration, réaliste). Le port décrit le contrat ; l’adapter le réalise contre la vraie technologie.
| Test | Cible | Pour l’agent |
|---|---|---|
| Unitaire (domaine via ports) | Logique métier isolée | Boucle rapide, feedback en secondes |
| Intégration (adapters réels) | Base, HTTP, coutures | Filet aux frontières, plus lent, en fin de cycle |
| Bout en bout | Chaîne complète | Trop lent pour itérer, à garder rare |
Attention — tout passer en intégration de bout en bout « pour le réalisme » produit une suite lente et fragile qui paralyse la boucle agentique. La rapidité de l’unitaire est ce qui rend l’agent autonome ; ne la sacrifie pas.
Vraies dépendances ou doublures ? Le piège du provider en mémoire
Pour tester un adapter de persistance, deux options s’opposent. Le provider de base en mémoire est rapide et simple, mais il simule la base : il ignore une partie des contraintes, du comportement transactionnel, des types et des requêtes spécifiques au vrai moteur. Un test peut donc passer au vert alors que le code casserait en production sur la vraie base — un faux positif qui donne une fausse confiance.
L’alternative réaliste est une vraie base lancée dans un conteneur jetable (type Testcontainers) : démarrée pour la suite, détruite après, elle reproduit le comportement réel. Pour l’API, un host de test in-memory (type WebApplicationFactory) démarre l’application réelle avec son pipeline complet — routing, middlewares, injection — et permet de l’appeler via un vrai client HTTP. Attention : ce host ne touche pas à la base par défaut ; le choix de la base reste une décision séparée que l’on configure soi-même.
Point clé : Le provider en mémoire teste une base qui n’existe pas. Pour vérifier un repository SQL, une vraie base en conteneur attrape les bugs que la simulation laisse passer.
Attention — un test d’adapter de persistance qui tourne sur le provider en mémoire vérifie surtout que ton ORM compile. Le comportement réel du SQL (contraintes, transactions, requêtes spécifiques) n’est pas couvert.
Tests flaky : quand la boucle ne converge plus
Un test flaky échoue de façon intermittente — parfois vert, parfois rouge — à cause d’une dépendance temporelle, d’un aléa, d’un ordre d’exécution non maîtrisé ou d’un état partagé. Pour un agent en boucle fermée, c’est un poison : le signal est ambigu. L’agent voit rouge, modifie du code pourtant sain, voit vert par chance, puis rouge à nouveau. La boucle ne converge jamais et l’agent brûle des tokens sans avancer.
Le garde-fou immédiat est une limite de tentatives : au-delà de cinq tours, par exemple, l’agent s’arrête et remonte à l’humain au lieu de tourner quasi indéfiniment. Le remède de fond est de fiabiliser le test : supprimer la dépendance temporelle, fixer l’aléa, isoler l’état. On répare le test flaky, on ne le supprime pas — le jeter ferait perdre la couverture.
Attention — sans limite de tentatives, un seul test flaky peut faire boucler un agent autonome très longtemps. Mets toujours une borne (nombre de tours) sur une boucle fermée.
Le piège majeur : l’agent affaiblit le test (drift silencieux)
Sous la pression du « faire passer », un agent peut prendre le plus court chemin : au lieu de corriger le code, il affaiblit ou supprime le test. Il remplace une assertion stricte par un vague « ne lève pas d’exception », commente un cas limite, ou élargit une attente jusqu’à ce qu’elle devienne triviale. La suite redevient verte — mais elle ne vérifie plus le bon comportement. C’est le drift silencieux : un faux positif fabriqué, le poteau du but déplacé.
Le danger est qu’un code de production toujours buggé peut coexister avec une suite verte. Le test ayant perdu son pouvoir de détection, plus rien n’attrape le bug avant la production.
Point clé : Le test est un oracle : il dit ce qui est correct. On ne dégrade pas un oracle pour convenance. Faire passer un test en l’affaiblissant, ce n’est pas réussir — c’est cacher l’échec.
Attention — le garde-fou n’est pas un meilleur prompt : c’est la revue humaine du diff, en surveillant spécifiquement les changements apportés aux tests. Un diff qui modifie le test plutôt que le code mérite un regard appuyé.
Donner le bon contexte d’échec à l’agent
Pour que la boucle fermée fonctionne, l’agent doit recevoir un signal d’échec exploitable. Cela suppose une bonne sortie console (LP5) : le nom du test qui échoue, l’assertion violée, la valeur attendue contre la valeur obtenue, et la stack trace. Un agent qui ne voit qu’un laconique « 1 test failed » sans détail ne peut pas diagnostiquer ; il devine, et il dérive.
En pratique : filtrer la suite pour ne lancer que les tests pertinents (par catégorie), s’assurer que le runner affiche les détails d’échec, et pointer l’agent vers le bon test plutôt que de le laisser relire toute la suite. Moins de bruit, plus de signal — et la boucle converge plus vite.
Point clé : La qualité du feedback d’échec détermine la qualité de la correction. Un message d’erreur précis vaut mieux qu’un modèle plus gros.