Le WordPress d'aujourd'hui, décodé pour les développeurs

Tests

setUpBeforeClass et tearDownAfterClass : l’état partagé qui piège vos tests

Un test passe seul et échoue en suite complète ? La cause se cache souvent dans une méthode statique exécutée une seule fois par classe.

Par WordPress Développement • 12 mars 2020 • 5 min de lecture • Aucun commentaire
setUpBeforeClass et tearDownAfterClass : l'état partagé qui piège vos tests

phpunit --filter test_publie_larticle passe sans problème. La suite complète, elle, échoue sur ce même test, une fois sur trois, sans qu’aucune ligne de code n’ait changé entre les deux exécutions. Ce scénario, classique dans les équipes qui grandissent, met souvent plusieurs heures à être compris avant qu’on ne pense à regarder du côté des méthodes exécutées une seule fois par classe.

La cause, une fois identifiée, est presque toujours la même : une donnée créée dans setUpBeforeClass reste vivante en mémoire pendant toute la durée d’exécution de la classe de test, et un test suivant modifie cette donnée sans que le test précédent s’en aperçoive. Comprendre le cycle de vie exact de ces méthodes évite ce genre de piège.

Deux niveaux de préparation, deux fréquences différentes

PHPUnit distingue deux couples de méthodes de préparation et de nettoyage. setUp et tearDown s’exécutent avant et après chaque méthode de test individuelle : leur état ne survit jamais d’un test à l’autre, à condition qu’il soit stocké dans une propriété d’instance classique. setUpBeforeClass et tearDownAfterClass, en revanche, s’exécutent une seule fois pour l’ensemble de la classe, avant le premier test et après le dernier.

class ArticleRepositoryTest extends WP_UnitTestCase {

    private static array $categories_partagees = [];

    public static function setUpBeforeClass(): void {
        parent::setUpBeforeClass();
        self::$categories_partagees = [ 'actu', 'tutoriel', 'analyse' ];
    }

    public function test_filtre_par_categorie(): void {
        // Un test modifie le tableau statique...
        array_pop( self::$categories_partagees );
        $this->assertCount( 2, self::$categories_partagees );
    }

    public function test_liste_toutes_les_categories(): void {
        // ...et ce test hérite du tableau déjà modifié.
        $this->assertCount( 3, self::$categories_partagees );
    }
}

Ces deux méthodes doivent obligatoirement être statiques, ce qui est le premier indice de leur portée : tout ce qu’elles initialisent dans une propriété statique de la classe est partagé par tous les tests, dans l’ordre où PHPUnit décide de les exécuter.

Pourquoi le test passe seul mais échoue en suite

L'essentiel à retenir : Une méthode exécutée une seule fois pour toute la classe ; Un état statique qui survit entre les tests ; Un correctif simple mais souvent oublié

Exécuté isolément, un test ne voit jamais l’effet d’un autre test sur l’état statique, puisque setUpBeforeClass repart de zéro à chaque nouvelle exécution du processus PHPUnit. En revanche, dans une suite complète, l’ordre d’exécution des méthodes détermine quel test modifie l’état en premier, et quel test en hérite ensuite. Ce comportement dépend :

  • De l’ordre par défaut, qui suit généralement l’ordre de déclaration dans le fichier, sauf configuration contraire.
  • D’options comme --order-by=random, qui rendent le problème visible de façon intermittente plutôt que systématique.
  • Du nombre de tests dans la classe : plus il y en a, plus la probabilité qu’un test modifie une donnée lue par un autre augmente.

C’est cette dépendance à l’ordre d’exécution qui rend le bug difficile à reproduire de façon fiable : deux développeurs sur deux machines différentes, ou deux exécutions successives sur la même machine, peuvent obtenir des résultats différents.

Le correctif

La solution la plus sûre consiste à ne jamais modifier, dans un test individuel, une donnée initialisée par setUpBeforeClass. Cette méthode doit servir uniquement à préparer des ressources coûteuses et véritablement en lecture seule pour toute la classe, par exemple une connexion réseau simulée ou un jeu de données de référence jamais modifié.

public function test_filtre_par_categorie(): void {
    $categories = self::$categories_partagees; // copie locale
    array_pop( $categories );
    $this->assertCount( 2, $categories );
}

En travaillant sur une copie locale plutôt que sur la référence statique, chaque test redevient indépendant des autres, indépendamment de l’ordre dans lequel PHPUnit décide de les exécuter.

Réinitialiser explicitement quand la mutation est nécessaire

Si un test doit réellement modifier une donnée partagée de façon contrôlée, il est possible de restaurer son état dans tearDown, exécuté après chaque méthode individuelle, plutôt que d’attendre tearDownAfterClass qui n’intervient qu’une fois à la toute fin.

protected function tearDown(): void {
    self::$categories_partagees = [ 'actu', 'tutoriel', 'analyse' ];
    parent::tearDown();
}

Cette approche fonctionne, mais elle ajoute une responsabilité supplémentaire à chaque test : ne jamais oublier de remettre l’état statique dans sa forme initiale. C’est pourquoi la première solution, éviter toute mutation de l’état de classe, reste préférable dès que possible.

Une propriété statique dans une classe de test doit être traitée comme une variable globale : utile pour partager une ressource coûteuse, dangereuse dès qu’un test la modifie sans la restaurer.

En résumé

Un test qui échoue uniquement en suite complète, jamais isolément, pointe presque toujours vers un état statique construit une fois par classe et modifié silencieusement par un test précédent. Réserver setUpBeforeClass aux données véritablement immuables, et travailler sur des copies locales plutôt que sur la référence statique directe, élimine cette classe entière de bugs intermittents.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Partager :

À propos de l'auteur

WordPress Développement

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

Voir tous ses articles

Dans la même veine

À lire aussi