Une suite de tests entièrement verte, 340 tests sur 340, a laissé passer en production une régression qui affichait le mauvais prix sur une variation de produit WooCommerce pendant près de dix jours avant d’être signalée par un client. En reprenant les tests censés couvrir ce module, aucun n’avait de syntaxe fautive, aucun n’était ignoré (skip) ni marqué incomplet : ils souffraient tous de défauts plus subtils, invisibles lors d’une revue de code rapide, qui les rendaient incapables de détecter la régression qu’ils étaient censés prévenir.
Voici les anti-patterns identifiés lors de cet audit, chacun avec ce qu’on voit en apparence, pourquoi c’est un problème réel, et comment corriger sans tout réécrire.
Ce qu’on voit : un test qui mocke la classe qu’il est censé tester
Sur trois tests de ce module, la classe testée elle-même était mockée partiellement, une pratique parfois utilisée pour isoler une méthode d’une autre au sein de la même classe, mais appliquée ici directement à la méthode contenant le calcul fautif.
// Ce qu'on voit : le test mocke la méthode qu'il devrait vérifier
$produit = $this->getMockBuilder( WC_Product_Variation::class )
->onlyMethods( [ 'get_price' ] )
->getMock();
$produit->method( 'get_price' )->willReturn( '24.90' );
$this->assertEquals( '24.90', $produit->get_price() );
Pourquoi c’est un problème : ce test vérifie que le mock répond ce qu’on lui a demandé de répondre, rien de plus. Il passera identiquement, que la vraie méthode get_price() du projet soit correcte ou totalement cassée, puisqu’elle n’est jamais réellement appelée.
Quoi faire : ne mocker que les dépendances externes réelles de la classe testée, jamais la méthode ou la classe qui contient le comportement qu’on cherche justement à vérifier. Si isoler une méthode d’une autre est nécessaire, cela signale souvent que la classe gagnerait à être découpée plutôt que testée avec des mocks partiels internes.

Ce qu’on voit : une assertion sur le HTML entier de la page
Un des tests d’intégration comparait la sortie HTML complète d’une page produit à une longue chaîne figée en dur dans le test :
$this->assertEquals( file_get_contents( 'fixtures/page-produit-attendue.html' ), $html_rendu );
Pourquoi c’est un problème : ce type d’assertion échoue à la moindre modification, même totalement anodine, d’un attribut CSS ou d’un espace dans un template, ce qui pousse l’équipe à mettre à jour la fixture mécaniquement sans vraiment relire ce qui a changé, dès la première fois que ça arrive. Après quelques mises à jour de ce genre, l’assertion devient un rituel vidé de sens : elle échoue souvent, on régénère la fixture par réflexe, et personne ne vérifie plus si le nouveau contenu figé est réellement correct.
Quoi faire : cibler des assertions précises sur les éléments qui comptent réellement pour le comportement testé, par exemple la présence du bon prix dans un sélecteur CSS donné, plutôt que la totalité du balisage généré. Réserver les comparaisons de snapshot complet aux cas où une revue humaine systématique du diff est effectivement garantie à chaque changement.
Ce qu’on voit : des données de test partagées entre plusieurs méthodes
Plusieurs tests du même fichier réutilisaient un produit créé une seule fois dans une méthode setUpBeforeClass, puis modifié différemment par chaque test suivant, dans l’idée de gagner du temps d’exécution en évitant de recréer un produit à chaque méthode.
public static function setUpBeforeClass(): void {
self::$produit_partage = self::factory()->post->create( [ 'post_type' => 'product' ] );
}
public function test_prix_standard() {
update_post_meta( self::$produit_partage, '_regular_price', '24.90' );
// ...
}
public function test_prix_solde() {
update_post_meta( self::$produit_partage, '_sale_price', '19.90' );
// ce test dépend silencieusement du prix régulier fixé par le test précédent
}
Pourquoi c’est un problème : l’ordre d’exécution des méthodes de test n’est pas garanti stable dans le temps, et selon la configuration ou une exécution partielle de la suite, ce couplage silencieux entre deux tests censés être indépendants produit des résultats différents, voire des faux échecs difficiles à reproduire localement.
Quoi faire : créer les données nécessaires dans setUp(), exécuté avant chaque méthode individuellement plutôt qu’une seule fois pour toute la classe, quitte à accepter un léger surcoût d’exécution. Ce surcoût, mesuré sur ce projet, s’est révélé négligeable face au temps perdu à déboguer des échecs intermittents inexplicables.
Ce qu’on voit : un test qui vérifie l’absence d’exception, rien de plus
public function test_recalcul_prix_variation() {
$this->expectNotToPerformAssertions();
recalculer_prix_variation( $produit_id );
}
Pourquoi c’est un problème : ce test confirme uniquement que la fonction ne lève pas d’exception, ce qui est une garantie minimale et ne dit strictement rien sur l’exactitude du calcul effectué, précisément le point qui a fauté en production dans l’incident cité en introduction.
Quoi faire : vérifier explicitement l’état après exécution, par exemple en relisant le prix effectivement enregistré en base après l’appel, avec une valeur attendue précise plutôt qu’une simple absence d’erreur.
Un test qui ne peut techniquement jamais échouer sur le point qu’il prétend vérifier est plus trompeur qu’une absence totale de test : il occupe une ligne dans le rapport de couverture sans offrir la moindre protection réelle.
Comment repérer ces anti-patterns lors d’une revue de code
- Se demander explicitement, pour chaque test, ce qui le ferait échouer, plutôt que ce qui le fait actuellement passer
- Se méfier de tout mock appliqué directement à la classe ou à la méthode testée elle-même
- Repérer les assertions génériques (
assertNotNull,expectNotToPerformAssertions) sur un comportement qui mériterait une valeur précise - Vérifier que
setUp()recrée bien un état neutre avant chaque méthode, sans dépendance à l’ordre d’exécution
En résumé
Ces quatre anti-patterns partagent un point commun : ils produisent une suite de tests verte et rassurante en apparence, sans offrir la protection qu’on attend d’elle. Aucun ne se détecte par une simple lecture de la syntaxe ou du taux de couverture affiché ; ils demandent de se poser systématiquement la question inverse de d’habitude : non pas « ce test passe-t-il ? » mais « ce test échouerait-il vraiment si le comportement changeait ? ». C’est cette question, posée méthodiquement sur chaque test existant, qui a permis de corriger le module fautif sans attendre un nouvel incident client pour s’en apercevoir.