Tester une fonction qui envoie un e-mail pose un problème évident : personne ne veut mille messages partis pour de vrai à chaque exécution de la suite de tests.
La réponse tient en un mot : on remplace l'envoi par un double, et on vérifie qu'il a bien été sollicité.
Définition
Un mock est une fausse implémentation qui prend la place d'une dépendance réelle pendant un test. Elle rend une réponse décidée d'avance et enregistre tout ce qu'on lui demande : combien d'appels, avec quels arguments, dans quel ordre.
import { expect, it, vi } from "vitest";
import { notifier } from "./notifications.js";
it("envoie un message par destinataire", () => {
const envoyer = vi.fn();
notifier(["lea@exemple.com"], envoyer);
expect(envoyer).toHaveBeenCalledTimes(1);
expect(envoyer).toHaveBeenCalledWith("lea@exemple.com");
});La fonction vi.fn() ne fait rien et rend undefined, ce qui suffit ici. Sa valeur est ailleurs : elle garde la trace de ses appels, et c'est cette trace que le test interroge.
Quatre doubles, souvent confondus
| Nom | Ce qu'il fait |
|---|---|
| Espion | Observe les appels sans changer le comportement réel |
| Bouchon | Rend une réponse fixée, sans aucune logique |
| Mock | Un bouchon dont les appels sont ensuite vérifiés |
| Faux | Une implémentation simplifiée mais réellement fonctionnelle |
Le vocabulaire courant appelle tout cela « mock », et ce raccourci est sans grande conséquence. La distinction utile reste celle du dernier : un faux, comme une base en mémoire, fonctionne vraiment, là où les trois autres se contentent de répondre.
Remplacer un module, arrêter le temps
Passer la dépendance en argument est la voie la plus simple, mais elle n'est pas toujours possible. Vitest sait aussi intercepter un import entier, et remplacer l'horloge du moteur.
vi.mock("./mailer.js", () => ({
envoyer: vi.fn().mockResolvedValue({ id: "msg_1" }),
}));
vi.useFakeTimers();
relancerDansUneHeure();
vi.advanceTimersByTime(3600 * 1000);
expect(envoyer).toHaveBeenCalledTimes(1);L'horloge simulée est le remède aux tests lents : sans elle, vérifier une relance planifiée avec setTimeout() obligerait à attendre réellement une heure.
Questions fréquentes
Faut-il simuler toutes les dépendances ?
Non, seulement celles qui sortent du programme : réseau, disque, horloge, aléatoire, paiement. Une fonction de calcul importée par une autre n'a aucune raison d'être remplacée, et la remplacer ne fait que rendre le test aveugle à leur incompatibilité éventuelle.
Que se passe-t-il quand la vraie dépendance change ?
C'est la faiblesse structurelle du procédé : le double continue de répondre comme avant, la suite reste verte, et la rupture n'apparaît qu'en production. Le garde-fou habituel est de compléter par quelques tests d'intégration qui, eux, appellent la vraie implémentation.
Comment simuler un appel réseau ?
Deux écoles. La plus rapide remplace fetch() par un double qui rend une réponse toute faite. La plus fidèle intercepte la requête au niveau du réseau simulé, ce qui laisse passer le vrai code de l'Axios ou du client utilisé. La première suffit pour un test unitaire, la seconde vaut mieux dès qu'on veut vérifier des en-têtes ou un code d'erreur.