Sous-titre : L’IA a rendu la rédaction de tests peu coûteuse. Le plus difficile est désormais de choisir ce qui mérite un oracle.
Votre IA rédige des tests. Ont-ils une quelconque utilité ?
Il ne s'agit pas d'une figure de style. C'est la question que Fabien Potencier a soulevée lorsqu'il a affirmé que les tests faibles constituent désormais un handicap. Les agents de codage modifient le code plus vite que n'importe quel relecteur humain ne peut suivre. La suite de tests est la garantie à chaque modification. Si elle ne prouve que que les mocks ont été appelés dans l'ordre, que await() s'est exécuté une seule fois, ou qu'un objet ressemblant à un DTO a été renvoyé, alors cette garantie n'est que du vent.
Sebastian Bergmann exprime la même idée, mais avec d'autres termes. Derrière chaque barre verte se cache un oracle de test : la procédure qui détermine la validité d'un résultat. La réussite d'un test ne signifie pas que le produit est correct. Elle signifie simplement que l'oracle que vous avez choisi – consciemment ou non – a été satisfait.
Cet article n'est pas un tutoriel PHPUnit. Ce n'est pas un exposé sur le TDD. Ce n'est pas une comparaison entre ReactPHP et Amp.
Il s'agit d'une présentation subjective d'un petit projet connexe — nolife-tests (sous Darkwood content/) — qui utilise Darkwood Flow pour concrétiser une affirmation architecturale :
Les tâches représentent le comportement métier. Les pilotes sont des éléments de l'environnement d'exécution. Les suites applicatives devraient privilégier les premières et ignorer les seconds.
Si, après avoir lu cet article, vous êtes toujours fier de tous les tests réussis de votre dépôt, alors j'ai échoué. Si, en revanche, vous en supprimez discrètement quelques-uns ou réécrivez leurs assertions, alors l'expérience a fonctionné.
L'économie s'est inversée
Avant l'arrivée des agents, les tests de qualité coûtaient cher. Leur rédaction prenait du temps, et personne ne voulait s'en charger. Les équipes les ont donc ignorés et ont accepté le risque.
Aujourd'hui, écrire des tests est peu coûteux. Les agents n'hésitent pas à inonder une requête de tests de couverture. Potencier souligne que l'ancien reproche — « les agents écrivent des assertions et des mocks vides à tous les niveaux » — est largement dépassé avec les modèles actuels. Les agents peuvent également compléter les suites de tests réelles : identifier les comportements non assertés, les identifier et générer une erreur justifiée.
Les tests sont donc résolus ?
Non.
Ce qui est devenu rare, c'est le jugement : quels comportements méritent d'être assurés, quelles assertions survivent à une refactorisation, quels doublons masquent un défaut de conception, quelles barres vertes seraient encore validées si le sens du produit était compromis.
Cette distinction constitue l'article tout entier.
Before AI → Cost of writing tests ≫ Cost of choosing oracles
With coding agents → Cost of writing tests ≪ Cost of choosing oracles
Le volume sans discernement est une fausse assurance. La rapidité sans jugement est la garantie d'un confort illusoire qui s'avère inefficace tant pour la production que pour les agents.
À quoi ressemble un « test sans vie »
Je l'appelle tests sans vie : une suite de tests « verts » qui vérifie l'implémentation, l'ordre des appels et les mécanismes d'exécution, sans se soucier du fonctionnement métier. Les tests sont actifs dans l'intégration continue. L'assurance, elle, est morte.
Le dépôt associé contient une suite de tests tests/Bad/ à vocation pédagogique. Exécutez-la :
cd nolife-tests
composer install
composer test:bad # green, low value
composer test:good # behavioral oracles
Trois doublures. Même méthode de travail. Trois façons différentes de mentir.
1. Couplage de l'implémentation — simulations à tous les niveaux
Le test ImplementationCoupledTest construit un flux à partir de quatre instances simulées de JobInterface et vérifie qu'elles ont été appelées dans l'ordre avec des valeurs de retour prédéfinies. Il ne demande jamais la signification de l'extrait.
$fetch->expects($this->once())->method('__invoke')->with($request)->willReturn($raw);
$parse->expects($this->once())->method('__invoke')->with($raw)->willReturn($parsed);
$excerpt->expects($this->once())->method('__invoke')->with($parsed)->willReturn($draft);
$validate->expects($this->once())->method('__invoke')->with($draft)->willReturn($result);
$flow = (new Flow($fetch))->fn($parse)->fn($excerpt)->fn($validate);
($flow)(new Ip($request));
$flow->await();
$this->assertTrue(true); // Green. We never asserted meaning.
Remplacez la véritable fonction ParseJob par une implémentation défectueuse. Déployez une analyse HTML de piètre qualité. Cette suite reste fonctionnelle. Les simulations n'appellent jamais la fonction réelle.
Posez-vous la question du test de résistance : Si une version défectueuse de ParseJob était déployée, ce test resterait-il positif ? Oui. L’assurance porte donc sur l’ordre d’appel sous doubles, et non sur la fidélité de l’extrait.
2. Couplage en temps réel — test du mobilier
RuntimeCoupledTest encapsule FiberDriver et compte les appels à await(). Cela prouve que l'encapsuleur de boucle d'événements a été utilisé. Cela ne prouve pas que l'extrait est fidèle.
public function await(array &$stream): void
{
++$this->counter->awaitCalls;
$this->inner->await($stream);
}
// ...
$this->assertSame(1, $counter->awaitCalls);
ReactPHP n'a pas besoin de vos tests d'application. Amp non plus. L'intégration continue de Flow peut entraîner la destruction de pilotes. Votre suite de tests ne devrait pas enregistrer le nombre de fois où le pilote a consommé sa bande passante.
L'association Fibre → Ampli invaliderait-elle ce test même si l'extrait est correct ? Oui. C'est la définition même d'un test de mobilier.
- Théâtre de couverture — forme sans signification
Le test CoverageTheaterTest vérifie que instanceof ExcerptResult, un titre non nul et un extrait de type chaîne de caractères. Un agent apprécie ce modèle : couverture élevée, aucun oracle. Modifiez l'algorithme d'extrait pour qu'il renvoie « lorem ipsum » et l'intégration continue restera au vert.
$this->assertInstanceOf(ExcerptResult::class, $result);
$this->assertNotNull($result->title);
$this->assertIsString($result->excerpt);
// Never asked: is the excerpt faithful to the document?
Bergmann reprend le raisonnement suivant : l’oracle a choisi la « forme ». La forme n’est pas la vérité du produit.
Flux en un seul diagramme : Tâches vs Conducteurs
Darkwood Flow n'est pas une bibliothèque DAG nœuds/arêtes. Le travail se déplace sous forme de paquets d'informations (« Ip ») à travers une séquence composée de Jobs. La concurrence est gérée par la planification des paquets (« IpStrategy ») et un moteur d'exécution de coroutines configurable (« Driver »).
flowchart TD
Req[DocumentRequest] --> Fetch[FetchJob]
Fetch --> Parse[ParseJob]
Parse --> Excerpt[ExcerptJob]
Excerpt --> Validate[ValidateJob]
Validate --> Result[ExcerptResult]
subgraph behavior [Business behavior — test this]
Fetch
Parse
Excerpt
Validate
end
subgraph furniture [Runtime furniture — do not pin in app tests]
D1[FiberDriver]
D2[AmpDriver]
D3[ReactDriver]
D4[… future runtime]
end
behavior -.->|scheduled by| furniture
| Concept | Signification | Appartient aux tests d'application ? |
|---|---|---|
| Tâche | Transformer T1 → T2 (analyse, extraction, validation) |
Oui — oracles d'unités et de flux de travail |
| IP | Support immuable des données courantes | Indirectement (paquets d'entrée/sortie) |
| Flux | Étapes ordonnées composées avec fn
|
En tant qu'oracle de flux de travail, et non via des simulations d'ordre d'appel |
| Pilote | Implémentation de l'asynchrone | Pas de fumée dans l'intégration continue de Flow |
| Port | Limite d'E/S (DocumentSource) |
Stub ici |
L'architecture intègre déjà les enseignements tirés des tests. Le comportement métier est défini par les Jobs. L'environnement d'exécution est remplaçable. Si vos tests échouent lorsque vous changez de pilote, c'est qu'ils n'ont jamais testé le produit.
Dans le projet associé, le flux de travail est assemblé une seule fois :
return (new FlowFactory($driver))->create(static function () use ($source, $maxExcerptLength, $minExcerptLength) {
yield new FetchJob($source);
yield new ParseJob();
yield new ExcerptJob($maxExcerptLength);
yield new ValidateJob($minExcerptLength);
});
Même chaîne. Transmettez FiberDriver ou AmpDriver. Les tâches ne savent pas lequel.
php bin/excerpt.php fiber
php bin/excerpt.php amp
Même titre. Même extrait. Mobilier différent.
LĂ oĂą vivent les entreprises : testez les emplois
ParseJob, ExcerptJob et ValidateJob sont des fonctions ordinaires. Aucun flux ni pilote n'est requis. C'est là tout l'intérêt.
JobBehaviorTest affirme une signification :
$parsed = (new ParseJob())(new RawDocument('doc://1', $html));
$this->assertSame('Hello Flow', $parsed->title);
$this->assertSame('Jobs transform packets. Drivers schedule them.', $parsed->body);
$draft = (new ExcerptJob(40))(new ParsedDocument('doc://1', 'T', $longBody));
$this->assertSame('Jobs transform information packets into…', $draft->excerpt);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('too short');
(new ValidateJob(20))(new ExcerptDraft('doc://1', 'T', 'too short'));
Ces tests resteraient valides même si Flow disparaissait demain. Ils resteraient valides si vous exécutiez les tâches dans un worker Symfony Messenger, un script CLI ou un gestionnaire Framework X. Ils protègent les transformations observables, et non l'infrastructure d'orchestration.
Voici la première moitié du modèle mental :
Si vous pouvez tester unitairement une tâche sans construire de flux, vous avez trouvé le comportement métier.
Les flux de travail favorisent cette séparation. Lorsque les étapes sont nommées transformations avec des paquets typés, la question « que devons-nous affirmer ? » cesse d'être abstraite. On affirme la signification du paquet après l'étape.
L'oracle du flux de travail
Les tâches unitaires sont nécessaires, mais pas suffisantes. La composition peut toujours être erronée : ordre incorrect, validation manquante, port jamais utilisé.
WorkflowBehaviorTest exécute la chaîne complète à travers Flow et vérifie la fidélité par rapport à fixtures/article.html :
$result = FlowCollector::run($flow, new Ip(new DocumentRequest('fixture://article')));
$this->assertSame('Darkwood Flow Notes', $result->title);
$this->assertStringContainsString('Jobs transform information packets', $result->excerpt);
$this->assertStringContainsString('Drivers schedule', $result->excerpt);
$this->assertLessThanOrEqual(121, mb_strlen($result->excerpt));
Un deuxième cas alimente un document minimal et s'attend à un échec de validation — et non à une vérification de forme conforme aux attentes.
Notez ce qui n'est pas affirmé : l'ordre des appels, le type de pilote, le nombre d'appels await(), les attentes simulées sur les tâches.
Honnêteté concernant Ip
Ip::$data est en lecture seule. Flow ne modifie pas votre paquet d'origine ; chaque étape encapsule une nouvelle instance de Ip. Prétendre que le paquet d'entrée a changé est une erreur fréquente.
FlowCollector ajoute une tâche terminale qui stocke la charge utile finale — la même honnêteté que celle utilisée par les propres tests de Flow :
$flow->fn(static function (mixed $data) use ($box): mixed {
$box->value = $data;
return $data;
});
($flow)($ip);
$flow->await();
return $box->value;
Liste de souhaits Flow optionnelle (non requise pour l'argument) : une fonction d'assistance run(Ip): mixed synchrone pour les tests. En attendant, collectez explicitement. N'inventez pas de mutations qui n'existent pas dans le modèle.
Remplacez l'environnement d'exécution. Conservez les tests.
Voici la conclusion de ce dépôt — et de cet article.
MultiDriverBehaviorTest exécute des assertions identiques sous Fiber et Amp :
#[DataProvider('provideDrivers')]
public function testSameExcerptUnderDifferentDrivers(DriverInterface $driver): void
{
$flow = (new ExcerptWorkflowFactory())->create($driver, $source, maxExcerptLength: 120);
$result = FlowCollector::run($flow, new Ip(new DocumentRequest('fixture://article')));
$this->assertSame('Darkwood Flow Notes', $result->title);
$this->assertStringContainsString('Jobs transform information packets', $result->excerpt);
$this->assertStringEndsWith('…', $result->excerpt);
}
public static function provideDrivers(): iterable
{
yield 'fiber' => [new FiberDriver()];
yield 'amp' => [new AmpDriver()];
}
flowchart LR
Oracle[Same behavioral oracle] --> Fiber[FiberDriver]
Oracle --> Amp[AmpDriver]
Fiber --> Green1[Green]
Amp --> Green2[Green]
Bad[await / driver spies] --> Fiber
Bad --> Red[Red after swap]
Si la modification de l'environnement d'exécution nécessite la réécriture des tests, ces derniers étaient liés à l'implémentation.
Cette phrase s'applique au-delà de Flow :
| Si vous passez de… | …et que votre suite dysfonctionne alors que la signification du produit reste inchangée… |
|---|---|
| PHP séquentiel | Vous testiez la pile d'appels, pas le résultat |
| ReactPHP | Vous testiez des promesses / des boucles |
| Amp | Vous testiez les types Amp, pas les paquets de domaine |
| Câblage du gestionnaire Framework X | Vous testiez l'adaptateur HTTP |
| Toute exécution future | Même diagnostic |
C’est pourquoi un article comparant « ReactPHP et Amp » n’est pas pertinent pour les tests. Les performances, l’expérience utilisateur et l’écosystème sont essentiels au choix de l’environnement d’exécution. Ils ne devraient quasiment jamais servir de critères d’évaluation pour une application.
La conception de Flow rend l'expérimentation peu coûteuse : le pilote est injecté à la limite de l'usine. Les tâches restent portables. La suite logicielle reste stable.
Simulations sous agents
Les simulations ne sont pas mauvaises en soi. Ce sont les simulations non examinées, réalisées sous l'influence de la vitesse de l'agent, qui le sont.
La distinction entre stub et mock chez Bergmann prend tout son sens lorsqu'un agent peut générer cinquante objets d'attente par minute. Un stub au niveau d'un port remplace une limite d'E/S par une simulation contrôlée et conserve les tâches réelles. Une chaîne factice remplace les collaborateurs par des objets d'attente et masque souvent le comportement que l'on souhaitait protéger.
Dans nolife-tests, le port est DocumentSource. La bonne suite de tests le simule :
$source = new InMemoryDocumentSource(['fixture://article' => $html]);
La tâche FetchJob est toujours en cours d'exécution. La tâche ParseJob est toujours en cours d'exécution. Le réseau, lui, ne fonctionne pas.
Contrairement à la simulation de FetchJob elle-même : vous interrompez l’exécution du mappage URI → RawDocument. Vous invitez également l’agent à simuler la tâche suivante, et la suivante, jusqu’à ce qu’il ne reste plus rien de réel — les « simulations à outrance » de Potencier, désormais produites à la vitesse de la machine.
Règle empirique utilisée dans les compétences associées :
| À faire | À ne pas faire |
|---|---|
Utiliser DocumentSource comme stub / utiliser InMemoryDocumentSource
|
Simuler DriverInterface dans les tests d'application |
Test unitaire des transformations App\Job\*
|
Vérification de l'ordre des appels simulés dans la chaîne de tâches |
Exécuter composer test:good comme CI requise |
Utiliser composer test:bad comme assurance |
Face à la multiplication des mocks, demandez-vous s'ils ne masquent pas un problème de conception. Si vous ne pouvez pas nommer un port, il se peut qu'il n'y en ait pas ; dans ce cas, le test invente une isolation que l'architecture n'a jamais justifiée.
Testez la décision en conditions réelles, ne générez pas la suite
La compétence pressure-test-decisions de Guillaume Moigneu est un protocole socratique pour les choix difficiles : cadrer la décision, auditer les hypothèses, générer des options réelles, forcer la clôture dans un enregistrement de conservation / d’expérimentation / d’information-action.
Les agents ne doivent pas se contenter d'écrire des tests. Ils doivent analyser en profondeur la décision de test avant d'afficher davantage de barres vertes.
Le dépôt associé fournit cette compétence et y ajoute une spécialisation : pressure-test-testing-decisions. Même discipline ; le domaine consiste à conserver, réécrire, supprimer et expérimenter sur les tests, les mocks, les oracles et le couplage d'exécution.
npx skills add . --list
# pressure-test-decisions
# pressure-test-testing-decisions
Exemples de requêtes pour ce dépôt :
Use $pressure-test-testing-decisions on tests/Bad/ImplementationCoupledTest.php —
should we keep, rewrite, or delete it?
Use $pressure-test-testing-decisions: stub DocumentSource or mock FetchJob?
Le vocabulaire technique correspond intentionnellement à l'article :
| Terme | Signification |
|---|---|
| Oracle | Assertion qui échoue si le sens métier est erroné |
| Mobilier | Détails d'exécution / pilote / commande d'appel qui ne concernent pas le produit |
| Théâtre de couverture | Vérifications de forme / null / instanceof qui restent vertes en cas de signification incorrecte |
| Remplacement de pilote | Même flux de travail sous Fibre vs Amp ; les tests en entreprise sont maintenus |
Voici un résumé de la session tirée des exemples du dépôt, qui se termine ainsi pour le test lié à l'implémentation :
| Champ | Entrée |
|---|---|
| Décision | Conserver à des fins éducatives |
| Bug produit qui resterait vert | Fonction ParseJob défectueuse / signification d'extrait incorrecte |
| Action suivante | Confirmer que composer test:good est la tâche CI requise |
Voilà le changement : de « générer des tests » à « défendre la demande d’indemnisation ».
Le conseil de Potencier pour les nouveaux projets reste valable : laissez l’agent écrire le test en premier et observez-le échouer pour la bonne raison. La couche de test de pression ajoute la question préalable : est-ce une bonne raison de s’en préoccuper ?
Questions utiles à poser lors d'un barbecue, une à la fois :
- Devrait-on mĂŞme tester cela ?
- Quel comportement protégeons-nous ? — Ce test échouerait-il pour une bonne raison ? — Cette assertion résiste-t-elle à une refactorisation ?
- Protégeons-nous l'architecture ou l'implémentation ?
- Ces maquettes masquent-elles un problème de conception ?
- Si nous remplacions la fibre par l'amplification, cela échouerait-il — et le devrait-il ?
- Dans six mois : suite verte, produit défectueux — quel test a menti ?
Que demander Ă un agent
Si vous ne deviez changer qu'une seule habitude après avoir lu ceci, modifiez les instructions que vous donnez à l'agent.
Cahier des charges insuffisant
Ajoutez des tests unitaires pour le flux de travail d'extraction. Visez une couverture élevée.
Cahier des charges solide
Tester la nécessité d'un nouveau test. Si oui, écrire un oracle comportemental pour la signification des tâches ou des flux de travail. Simuler
DocumentSource. Ne pas simuler les tâches. Ne pas effectuer d'assertions sur le pilote. Privilégier l'analyse de la fidélité des extraits avant toute modification d'implémentation.
Associer le brief aux suites du dépôt :
flowchart TD
Ask[Agent proposes a test] --> PT{Pressure-test decision}
PT -->|furniture / theatre| Del[Delete or park under Bad]
PT -->|Job meaning| Job[JobBehaviorTest style]
PT -->|composition meaning| Wf[WorkflowBehaviorTest style]
PT -->|runtime claim| Swap[Prove it with MultiDriverBehaviorTest — same assertions]
Le TDD fonctionne toujours. Priorisez l'analyse du comportement des tâches. Ne vous laissez jamais espionner par les pilotes. Renforcez les oracles, pas les pourcentages de couverture.
Les avertissements connexes de Bergmann s'inscrivent dans la même perspective : les suites de tests lentes constituent un plafond de couverture pour les réviseurs LLM (les hypothèses abandonnées ne donnent lieu à aucun ticket) ; « plus rapide que la compréhension » signifie que les agents saisissent en quelques minutes ce que les humains mettent des heures à vérifier ; les tests non modifiés ne constituent que la moitié de la preuve — si une correction réécrit la suite, il faut diagnostiquer le couplage avant de se réjouir.
Limites honnĂŞtes
Ce document ne prouve pas tout. Nommer les limites permet de maintenir l'honnêteté du débat.
-
Aucun pilote de synchronisation n'est encore disponible dans Flow pour une commande
run(Ip)en une seule ligne.FlowCollectorest la solution de contournement explicite. - Les stubs aux ports restent utiles. L'abandon de tous les doublons n'est pas la revendication. L'abandon des chaînes factices qui effacent les tâches l'est.
- Les bibliothèques de pilotes nécessitent toujours des tests — dans le package Flow, pas dans chaque suite applicative. Il faut tester systématiquement chaque pilote dans l'intégration continue du produit uniquement si le produit est le pilote.
-
Les performances de l'amplificateur par rapport à la fibre n'ont pas été mesurées. L'important, c'est la pérennité des oracles, pas un point de repère.
Les stratégies de concurrence (comme
MaxIpStrategy) doivent être définies dans les contrats de Flow. Les tests d'application ne doivent prendre en compte la concurrence que lorsque celle-ci constitue le comportement du produit. - L'IA peut rédiger de bons tests. Le risque réside dans le volume sans oracles — une fausse assurance due à la rapidité des agents — et non dans l'impossibilité d'en trouver de véritables.
Ce que le prototype montre : un flux de travail lisible, deux environnements d’exécution, trois éléments verts inutiles, trois oracles comportementaux et des compétences qui mettent à l’épreuve les principes de conservation/réécriture/suppression avant l’apparition de nouveaux fichiers PHPUnit.
Clonez-le. Cassez-le. Décidez.
Le dépôt n'est pas une annexe. Il constitue le laboratoire de cet article.
composer install
composer test:good # behavioral insurance
composer test:bad # educational false insurance
php bin/excerpt.php fiber
php bin/excerpt.php amp
Ouvrez ensuite tests/Bad/ImplementationCoupledTest.php, interrompez volontairement le véritable ParseJob et observez quelles suites de tests le détectent. Cette expérience de dysfonctionnement du code est plus fructueuse qu'un simple rapport de couverture.
Explorez le répertoire skills/ et effectuez un test de résistance sur l'un de vos propres tests écologiques. Conservez le compte rendu de la décision. Supprimez tout ce qui ne fait que polir les meubles.
Conclusion
La plupart des tests unitaires dans les bases de code modifiées par des agents testent la mauvaise chose — non pas parce que PHPUnit est faible, mais parce que l'oracle a choisi l'implémentation et l'exécution plutôt que le sens.
Flow définit déjà la limite : les tâches transforment les paquets ; les pilotes les planifient. Les flux de travail rendent le comportement visible sous forme d’une chaîne de transformations nommées. Cette visibilité est un atout pour les tests si vous l’utilisez, mais un piège si vous la masquez complètement.
L'IA a bouleversé l'économie. La rédaction des tests n'est plus le principal obstacle. Le véritable défi, c'est le choix d'experts fiables.
Une assurance qui résiste à un changement de conducteur est une assurance sur votre produit. Tout le reste n'est que du cirage.
Votre IA rédige des tests. Assurez-vous qu'ils soient pertinents.
Sources
— Sebastian Bergmann — Voir la vérité : tester les oracles
— Sebastian Bergmann — L'intervention factice/de simulation
— Sebastian Bergmann — Plus rapide que la compréhension
— Sebastian Bergmann — La vitesse comme facteur de sécurité
— Sebastian Bergmann — Au-delà des meilleures pratiques
- Guillaume Moigneu — pressure-test-decisions (Agent Skills)
- Darkwood — Documentation Flow
- Dépôt Github — Tests NoLife
Top comments (0)