Pourquoi une suite de tests, verte la veille, affiche-t-elle un échec le lendemain matin, sans qu’aucune ligne de code source n’ait été modifiée entretemps ? Cette question, posée dans presque toutes les équipes qui font grandir leur suite de tests au-delà de quelques dizaines de méthodes, a une réponse récurrente : un test dépend, sans le savoir, d’un effet de bord laissé par un autre test exécuté juste avant lui.
PHPUnit n’exécute pas nécessairement les tests dans l’ordre où ils apparaissent dans le fichier, en particulier quand plusieurs classes sont concernées ou quand une option de randomisation est activée. Un test correctement isolé doit produire le même résultat quel que soit l’ordre dans lequel il s’exécute par rapport aux autres.
Le symptôme : un échec impossible à reproduire isolément
# Cette commande passe sans problème
phpunit --filter test_compte_les_articles_publies
# Cette commande, exécutant toute la suite, échoue parfois sur ce même test
phpunit
Ce contraste est le signal le plus fiable d’une dépendance cachée à l’ordre d’exécution. Si un test échoue uniquement en présence d’autres tests, la cause ne se trouve jamais dans ce test isolément, mais dans une interaction avec l’état laissé par un ou plusieurs tests précédents.
Les causes les plus fréquentes

- Une propriété statique de classe, initialisée une seule fois dans
setUpBeforeClass, modifiée par un test sans être restaurée avant le suivant. - Une option WordPress ou un transient modifié directement par un test, sans passer par les mécanismes de nettoyage automatique de
WP_UnitTestCase. - Un utilisateur défini comme utilisateur courant via
wp_set_current_user, jamais réinitialisé à zéro en fin de test. - Un cache d’objet en mémoire, interne à une classe de service, qui conserve une valeur calculée lors d’un test précédent et la réutilise à tort pour le test suivant.
Dans chacun de ces cas, le test qui échoue n’est pas fautif en lui-même : il subit simplement les conséquences d’un état que le test précédent n’a pas nettoyé correctement.
Reproduire le problème de façon fiable
phpunit --order-by=random --random-order-seed=1234
Cette option force PHPUnit à exécuter les tests dans un ordre aléatoire, mais reproductible grâce à la graine fournie. Relancer la suite plusieurs fois avec des graines différentes permet de faire apparaître ce type de dépendance de façon nettement plus rapide qu’en attendant qu’elle se manifeste naturellement dans l’ordre habituel d’exécution.
Un exemple concret de fuite d’état
class CompteurArticlesTest extends WP_UnitTestCase {
public function test_categorie_actualites_vide(): void {
$categorie_id = self::factory()->category->create( [ 'name' => 'Actualités' ] );
$this->assertSame( 0, get_term( $categorie_id )->count );
}
public function test_categorie_actualites_avec_articles(): void {
// Recherche par nom au lieu de créer une nouvelle catégorie dédiée :
// ce test dépend de l'existence d'une catégorie créée ailleurs.
$categorie = get_term_by( 'name', 'Actualités', 'category' );
self::factory()->post->create( [ 'post_category' => [ $categorie->term_id ] ] );
$categorie_mise_a_jour = get_term( $categorie->term_id );
$this->assertSame( 1, $categorie_mise_a_jour->count );
}
}
Le second test suppose implicitement que la catégorie « Actualités » créée par le premier test existe déjà. Exécuté seul, il échoue immédiatement avec une erreur sur un objet nul. Exécuté après le premier test, il fonctionne, mais uniquement parce que l’ordre d’exécution garantit, par hasard, la présence de cette donnée.
Le correctif : chaque test crée sa propre fixture
public function test_categorie_avec_articles(): void {
$categorie_id = self::factory()->category->create( [ 'name' => 'Actualités Locales' ] );
self::factory()->post->create( [ 'post_category' => [ $categorie_id ] ] );
$categorie_mise_a_jour = get_term( $categorie_id );
$this->assertSame( 1, $categorie_mise_a_jour->count );
}
En créant sa propre catégorie plutôt qu’en cherchant à réutiliser celle d’un autre test, ce test devient indépendant de tout ce qui l’entoure. Il produit le même résultat, qu’il soit exécuté seul, en premier, en dernier, ou dans un ordre totalement aléatoire.
La question à se poser systématiquement en écrivant un test n’est pas « ce test passe-t-il », mais « ce test passerait-il exactement de la même façon s’il était le seul test de toute la suite ».
Le rôle de WP_UnitTestCase dans l’isolation
Une bonne partie de cette isolation est déjà gérée automatiquement par WP_UnitTestCase, qui encapsule chaque test dans une transaction de base de données annulée à la fin, garantissant qu’aucune donnée créée ne persiste réellement entre deux tests. Ce mécanisme couvre les données en base, mais pas l’état en mémoire PHP, comme les propriétés statiques, les variables globales, ou les caches internes à un service, qui restent sous la responsabilité du développeur.
En résumé
Un test qui échoue uniquement en présence d’autres tests révèle presque toujours une dépendance implicite à un état laissé par un test précédent, en mémoire plutôt qu’en base de données. Utiliser l’exécution aléatoire pour révéler ce type de problème rapidement, puis garantir que chaque test construit sa propre fixture indépendante, élimine cette classe entière de bugs intermittents et rend la suite fiable quel que soit l’ordre d’exécution choisi.