vendredi 25 septembre 2026

À propos

Contact

Tests

Un switch_to_blog oublié fait fuiter le contexte multisite d’un test à l’autre

Un test qui lit ou écrit sur le mauvais site d'un réseau, presque toujours parce que restore_current_blog n'a jamais été appelé après un switch_to_blog.

Par Clément Hadrot • 26 août 2024 • 4 min de lecture • Aucun commentaire
Un switch_to_blog oublié fait fuiter le contexte multisite d'un test à l'autre

Symptôme. Sur un réseau multisite gérant douze sites clients, un test vérifiant le nombre d’articles publiés sur le site 3 échouait de façon totalement imprévisible : parfois il passait, parfois il retournait le nombre d’articles du site 5. Aucune modification du test lui-même ne changeait ce comportement. Exécuté seul, isolément, il passait systématiquement. Exécuté dans la suite complète, son résultat dépendait de l’ordre d’exécution des tests précédents, ce qui est le signal le plus caractéristique d’une fuite d’état entre tests.

Diagnostic : remonter la pile des switch_to_blog

La fonction switch_to_blog( $site_id ) change le contexte courant, y compris les tables globales $wpdb, pour pointer vers un autre site du réseau. Elle empile ce changement, et restore_current_blog() dépile le dernier changement effectué. Le problème apparaît dès qu’un test appelle switch_to_blog() sans passer par un restore_current_blog() garanti, en particulier si une assertion échoue ou qu’une exception est levée entre les deux appels : l’exécution s’arrête avant d’atteindre le restore_current_blog() prévu en fin de méthode.

public function test_compte_articles_site_5() {
    switch_to_blog( 5 );
    $articles = get_posts( [ 'post_type' => 'post' ] );
    $this->assertCount( 8, $articles ); // échoue ici si le compte réel est différent
    restore_current_blog(); // jamais atteint si l'assertion précédente échoue
}

Ce test, s’il échoue sur son assertion, laisse le contexte du site 5 actif pour tous les tests suivants de la suite, qui croient alors interroger le réseau global ou un autre site précis alors qu’ils lisent en réalité les données du site 5.

Confirmer l’hypothèse avec un log de la pile de sites

L'essentiel à retenir : Le symptôme est un test qui échoue seulement selon l'ordre d'exécution ; La cause est presque toujours une pile de switch_to_blog non vidée ; Le correctif systématique passe par try/finally

Pour confirmer ce diagnostic avant de corriger, on ajoute temporairement une vérification de l’état de la pile multisite entre chaque test, via la fonction ms_is_switched() et la variable globale interne exposée par le cœur :

public function tear_down() {
    global $_wp_switched_stack;
    if ( ms_is_switched() ) {
        error_log( sprintf(
            'ALERTE : pile switch_to_blog non vidée après %s, %d niveau(x) restants',
            $this->getName(), count( $_wp_switched_stack )
        ) );
    }
    parent::tear_down();
}

Ce log a confirmé l’hypothèse en quelques minutes : trois méthodes de test différentes laissaient la pile non vidée, toutes pour la même raison, une assertion placée avant le restore_current_blog() de fin de méthode.

Correctif : garantir le retour au contexte d’origine avec try/finally

public function test_compte_articles_site_5() {
    switch_to_blog( 5 );
    try {
        $articles = get_posts( [ 'post_type' => 'post' ] );
        $this->assertCount( 8, $articles );
    } finally {
        restore_current_blog();
    }
}

Le bloc finally s’exécute que l’assertion réussisse, échoue, ou qu’une exception imprévue survienne, garantissant que le contexte du site est toujours restauré avant que le test suivant ne démarre.

Prévention : un tearDown défensif dans la classe de base

Au-delà du correctif ponctuel, nous avons ajouté un filet de sécurité dans la classe de base partagée par tous les tests multisite du projet, qui force la restauration complète de la pile si jamais un test l’a laissée sale malgré tout :

abstract class Multisite_TestCase extends WP_UnitTestCase {
    public function tear_down() {
        while ( ms_is_switched() ) {
            restore_current_blog();
        }
        parent::tear_down();
    }
}
  • Ce filet ne remplace pas le try/finally dans chaque test, il évite seulement la contamination en cascade si un test l’oublie malgré tout.
  • Un test isolé qui passe mais échoue en suite complète doit toujours faire suspecter une fuite d’état globale, multisite ou non.
  • La commande phpunit --order-by=random aide à révéler ce genre de dépendance cachée entre tests, en variant l’ordre d’exécution d’une exécution à l’autre.

Pour aller plus loin

Ce billet ne traite pas de la configuration initiale d’une suite de tests en environnement multisite, seulement de ce piège précis une fois cette configuration en place. Le réflexe à retenir dépasse le cadre multisite : toute fonction qui empile un contexte temporaire (switch_to_blog, mais aussi wp_suspend_cache_addition ou un changement de locale) mérite le même traitement défensif par try/finally.

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