Isolons le code pour les tests : Que sont les mocks et espions ?

Dans les applications réelles, les fonctions sont rarement absolument pures. Elles dépendent du temps, des paramètres globaux, des bases de données ou font des requêtes à des API tierces. Si un serveur externe tombe, vos tests tomberont aussi, bien que votre code soit écrit parfaitement.

Pour briser ces liens et tester la logique en isolation complète, on applique des — des et des .

1. Métaphore des cascadeurs

Imaginez un tournage de film. Si selon le scénario le héros doit sauter d'un hélicoptère en feu dans un précipice, le réalisateur ne risquera pas la vie d'un acteur coûteux. À sa place, la cascade est exécutée par un cascadeur professionnel (double).

est un cascadeur dans le code. C'est une fonction vide qui remplace une dépendance réelle. Elle sait :

  1. Mémoriser combien de fois elle a été appelée.
  2. Mémoriser avec quels arguments spécifiques elle a été appelée.
  3. Retourner un résultat préparé à l'avance sans exécuter le code réel lourd ou dangereux.

2. Création de mocks dans Vitest avec vi.fn()

Supposons que nous avons une fonction qui traite une liste de commandes et envoie une notification pour chaque commande terminée via un callback :

function processCompletedOrders(orders, notifyCallback) {
    orders.forEach(order => {
        if (order.status === 'completed') {
            notifyCallback(order.id, order.amount);
        }
    });
}

Nous voulons vérifier précisément la logique de filtrage des commandes, sans envoyer de notifications réelles. Nous créons un mock-callback via vi.fn() :

import { test, expect, vi } from 'vitest';

test('processCompletedOrders appelle le callback seulement pour les commandes terminées', () => {
    // 1. Arrange (Créons une fonction mock)
    const mockNotify = vi.fn();
    const testOrders = [
        { id: 1, status: 'completed', amount: 150 },
        { id: 2, status: 'pending', amount: 300 },
        { id: 3, status: 'completed', amount: 500 }
    ];

    // 2. Act (Lançons la logique testée)
    processCompletedOrders(testOrders, mockNotify);

    // 3. Assert (Vérifions le comportement du mock)
    // Vérifions que le callback a été appelé exactement 2 fois
    expect(mockNotify).toHaveBeenCalledTimes(2);
    
    // Vérifions les arguments du premier appel
    expect(mockNotify).toHaveBeenNthCalledWith(1, 1, 150);
    
    // Vérifions les arguments du deuxième appel
    expect(mockNotify).toHaveBeenNthCalledWith(2, 3, 500);
});

3. Espions : vi.spyOn()

Parfois, nous n'avons pas besoin de remplacer le comportement de la méthode par un vide, mais nous voulons « l'espionner » — si la méthode de l'objet original est appelée et avec quels paramètres. Pour cela, on utilise des .

const database = {
    save(data) {
        // Une certaine écriture réelle complexe sur disque
        return true;
    }
};

// Créons un espion pour la méthode 'save' de l'objet 'database'
const saveSpy = vi.spyOn(database, 'save');

// Appelons la méthode réelle
database.save({ username: 'alex' });

// Vérifions les appels
expect(saveSpy).toHaveBeenCalledWith({ username: 'alex' });

// Important ! Retirons l'espion et restaurons la méthode originale
saveSpy.mockRestore();
Important

Règle d'or de l'utilisation des mocks : « Mockez seulement les frontières externes de votre système » (requêtes API, base de données, temps système intégré, bibliothèques npm tierces). Ne mockez pas vos helpers internes purs, sinon les tests cesseront de vérifier le travail réel de votre application.