Lors d’une revue de code, un développeur junior de l’équipe a commenté une ligne de test par « on mock l’appel API ici ». Le code ne faisait pourtant que retourner une valeur fixe, sans jamais vérifier que quoi que ce soit avait été appelé. Ce n’était pas un mock : c’était un stub. La confusion est fréquente, et elle n’est pas seulement une question de pédantisme — elle change la façon dont on écrit le test.
Ces trois notions répondent chacune à une question différente : que doit retourner cette dépendance (stub), a-t-elle bien été appelée comme prévu (mock), ou simplement a-t-elle été appelée, sans que cela conditionne la réussite du test (spy) ? Clarifier cette distinction évite d’écrire des tests qui vérifient la mauvaise chose.
Le stub : une valeur imposée, rien de plus
Un stub remplace une dépendance réelle par une version qui retourne toujours la même chose, sans aucune logique de vérification. Il sert à isoler le code testé d’une dépendance lente, coûteuse ou non déterministe — un appel réseau, une lecture disque, une horloge système.
class Test_Calcul_Livraison extends WP_UnitTestCase {
public function test_frais_livraison_zone_lointaine(): void {
$service_geo = $this->createStub(Service_Geolocalisation::class);
$service_geo->method('distance_km')->willReturn(850);
$calculateur = new Calculateur_Frais_Livraison($service_geo);
$this->assertSame(24.90, $calculateur->calculer('75001'));
}
}
Ici, peu importe combien de fois distance_km() est appelée, ni avec quels arguments : le test ne s’intéresse qu’au résultat final du calcul, pas au comportement du service de géolocalisation.
Le mock : une attente sur l’interaction elle-même
Un mock va plus loin : il fait échouer le test si l’appel attendu n’a pas lieu, ou n’a pas lieu avec les bons arguments. On l’utilise quand l’appel lui-même est la chose importante à vérifier — typiquement l’envoi d’un e-mail, l’écriture d’un log, ou le déclenchement d’un hook.
public function test_notification_envoyee_apres_commande(): void {
$notificateur = $this->createMock(Service_Notification::class);
$notificateur->expects($this->once())
->method('envoyer')
->with($this->equalTo('client@example.test'), $this->stringContains('confirmée'));
$service_commande = new Service_Commande($notificateur);
$service_commande->valider(42);
}

Si envoyer() n’est jamais appelé, ou l’est avec un mauvais destinataire, le test échoue immédiatement — même si le reste du comportement de valider() semble correct. C’est précisément l’inverse du stub : ici, l’interaction est l’assertion.
Le spy : observer sans imposer de contrainte
Le spy enregistre les appels réels sans en modifier le comportement, pour les inspecter après coup, sans pour autant faire échouer le test si l’appel n’a pas eu lieu dans un ordre précis. Dans un contexte WordPress, l’outil natif le plus proche du spy reste did_action() et le comptage d’appels de hook :
public function test_hook_declenche_apres_publication(): void {
add_action('billet_publie', function () {});
$this->factory()->post->create([
'post_type' => 'billet',
'post_status' => 'publish',
]);
$this->assertGreaterThan(0, did_action('billet_publie'));
}
Ce test observe qu’un hook s’est déclenché, sans imposer d’attente stricte sur le nombre exact d’appels ni sur les arguments — une distinction utile quand on veut simplement s’assurer qu’un comportement existe, sans figer une implémentation encore mouvante.
Un tableau pour trancher rapidement
| Doublure | Question posée | Fait échouer le test si absent ? |
|---|---|---|
| Stub | Que dois-je retourner ? | Non |
| Mock | Cet appel a-t-il eu lieu comme prévu ? | Oui |
| Spy | Que s’est-il passé, pour information ? | Non, sauf assertion explicite ensuite |
Pourquoi cette distinction évite des tests fragiles
Transformer systématiquement des stubs en mocks est une erreur courante : elle fige des détails d’implémentation qui n’ont pas besoin de l’être. Si un test vérifie qu’une méthode a été appelée exactement une fois alors que seule la valeur de retour compte réellement, le moindre refactoring interne — même sans changement de comportement observable — fait échouer la suite. À l’inverse, utiliser un stub là où un mock serait nécessaire laisse passer de vraies régressions, comme l’e-mail de confirmation qui ne part jamais.
La règle que nous appliquons : un mock se justifie uniquement quand l’effet de bord est la fonctionnalité elle-même. Dans tous les autres cas, un stub suffit et rend le test plus robuste aux refactorings.
Notre verdict
Ce vocabulaire n’est pas un exercice académique : il structure la façon de penser un test. Se demander « est-ce que je vérifie une valeur ou une interaction ? » avant d’écrire la moindre ligne de simulation évite la moitié des tests trop rigides rencontrés en revue de code. Les bibliothèques dédiées comme Mockery ou Brain Monkey offrent des API plus riches pour ces trois usages, mais la distinction conceptuelle reste identique quel que soit l’outil choisi.