# 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.

- Auteur : Clément Hadrot
- Publié le : 2024-08-26
- Mis à jour le : 2024-08-26
- Catégorie : Tests
- URL : https://wpmoderne.dev.wordpress-developpement.fr/tests/switch-to-blog-oublie-fuite-contexte-multisite/

## L’essentiel

- 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

**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`.
