vendredi 25 septembre 2026

À propos

Contact

Tests

Vos tests passent seuls mais échouent en suite : traquer une fuite d’état

Un test isolé est vert, la suite entière le fait rougir. Voici comment repérer la variable globale ou le hook oublié qui contamine le test suivant.

Par Clément Hadrot • 18 janvier 2020 • 5 min de lecture • Aucun commentaire
Vos tests passent seuls mais échouent en suite : traquer une fuite d'état

Un client signale qu’une extension de réservation « fonctionne en local mais casse en intégration continue ». Premier réflexe : relancer le test incriminé seul, avec phpunit --filter test_disponibilite_creneau. Il passe. Relancé dans la suite complète, il échoue une fois sur trois, toujours après le même voisin. Ce genre de symptôme a un nom précis : la fuite d’état entre tests.

Ce n’est pas un bug de logique métier, c’est un bug d’hygiène de test. Un test antérieur laisse une trace — une variable statique, une option WordPress modifiée, un hook accroché — et cette trace influence le comportement du test suivant. Le correctif est presque toujours plus simple que le diagnostic.

Confirmer que le symptôme est bien une fuite d’état

Avant de creuser, il faut éliminer les fausses pistes : dépendance externe non mockée, horodatage réel, ou ordre aléatoire activé par --random-order. Le test décisif consiste à relancer exactement les deux mêmes méthodes, dans le même ordre, en isolant tout le reste :

phpunit --filter '(test_creneau_disponible|test_disponibilite_creneau)' tests/test-reservations.php

Si l’échec réapparaît de façon reproductible dans cet ordre précis mais jamais dans l’ordre inverse, la fuite d’état est confirmée. C’est un signal fort : le problème ne vient pas du test qui échoue, mais de celui qui l’a précédé.

Cartographier les suspects habituels

Dans une base de code WordPress, trois catégories de coupables reviennent sans cesse :

  • Les propriétés statiques de classes, notamment les caches internes d’une classe de service instanciée une seule fois par la suite.
  • Les hooks ajoutés avec add_filter() ou add_action() dans un test et jamais retirés avec remove_filter() dans son tearDown().
  • Les options ou transients modifiés via update_option() sans restauration, alors que WP_UnitTestCase ne réinitialise que la base de données transactionnellement, pas les valeurs mises en cache par un objet PHP vivant en mémoire.
L'essentiel à retenir : Isoler un test pour confirmer le symptôme ; Traquer les statics et les hooks non nettoyés ; Nettoyer dans tearDown, pas dans le test suivant

Un outil pratique pour accélérer la chasse : la méthode tearDown() peut afficher, en cas d’échec, l’état des hooks actifs sur un filtre suspect via $GLOBALS['wp_filter']. C’est brutal, mais redoutablement efficace en debug ponctuel — à retirer ensuite, bien sûr.

Localiser précisément la ligne fautive

Une fois la catégorie identifiée, la recherche se resserre. Pour un hook qui traîne, un grep -rn "add_filter" tests/ combiné à une relecture des tearDown() correspondants suffit souvent. Pour une propriété statique, il faut repérer le pattern singleton :

class Cache_Disponibilites {
    private static ?array $cache = null;

    public static function get(): array {
        if (self::$cache === null) {
            self::$cache = self::calculer();
        }
        return self::$cache;
    }
}

Ce singleton, initialisé une fois par le premier test qui l’appelle, sert ensuite la même valeur à tous les tests suivants du même processus PHPUnit — même si la base de données, elle, a bien été réinitialisée entre chaque méthode.

Corriger sans casser la classe de production

La tentation est de supprimer le cache statique en production. Mauvaise idée : il existe probablement pour de bonnes raisons de performance. La correction se fait côté tests, avec une méthode de réinitialisation explicite :

class Cache_Disponibilites {
    // ... code existant ...

    public static function reset_pour_tests(): void {
        self::$cache = null;
    }
}

Puis, dans la classe de test :

protected function tearDown(): void {
    Cache_Disponibilites::reset_pour_tests();
    remove_all_filters('reservation_creneau_disponible');
    parent::tearDown();
}

La méthode reset_pour_tests() est volontairement nommée pour ne jamais être confondue avec une méthode métier — un préfixe explicite évite qu’un développeur pressé l’appelle par erreur ailleurs.

Dans notre équipe, la règle est simple : tout ce qu’un test ajoute à l’état global — hook, option, cache statique — doit être retiré dans le même fichier, par la même méthode de test. Aucune exception, même pour « juste un filtre ».

Prévenir la récidive

Trois habitudes réduisent drastiquement le risque de revoir ce problème :

  1. Exécuter la suite avec --order-by=random régulièrement en local, pas seulement en CI : un ordre différent à chaque exécution révèle vite les dépendances cachées.
  2. Écrire une méthode tearDown() systématique dans chaque classe de test qui touche un hook ou une classe à état, même si le test semble « propre ».
  3. Ajouter un test de garde qui vérifie qu’aucun filtre inattendu n’est encore accroché après l’exécution de la suite d’un module donné.

Ce dernier point est souvent négligé, alors qu’il coûte quelques lignes :

public function test_aucun_filtre_residuel_apres_suite(): void {
    $this->assertEmpty(
        $GLOBALS['wp_filter']['reservation_creneau_disponible'] ?? null
    );
}

En résumé

Une fuite d’état ne se manifeste presque jamais dans le test qui la provoque : elle attend patiemment le test suivant pour faire des dégâts. Le réflexe à adopter est donc de toujours suspecter un voisin avant de remettre en cause la logique du test qui échoue. Isoler, comparer l’ordre d’exécution, cartographier les singletons et les hooks, puis nettoyer méthodiquement dans tearDown() : cette séquence résout la grande majorité des cas rencontrés sur des projets d’agence multi-clients où les classes de service statiques sont légion. Ce sujet ne couvre volontairement pas l’isolation par processus séparé, qui répond à un problème voisin mais distinct — celui du temps d’exécution, pas de la reproductibilité.

Partager :

À propos de l'auteur

Clément Hadrot

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi