Passer au contenu

Techniques Avancées de Mocking avec Vitest

Le mocking est une technique essentielle pour isoler le code testé de ses dépendances. Ce tutoriel explore les concepts avancés de mocking dans Vitest que tout développeur devrait connaître.

Mock de Classes

Lorsque vous testez du code qui utilise des classes, vous aurez souvent besoin de simuler leur comportement. Vitest permet de mocker facilement des classes entières.

dog.js
export class Dog {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} says woof!`;
}
fetch() {
// logique complexe...
return "Got the ball!";
}
}
// dog.test.js
import { expect, test, vi } from 'vitest';
import { Dog } from './dog';
vi.mock('./dog', () => {
return {
Dog: vi.fn(() => ({
speak: vi.fn().mockReturnValue('Mocked woof!'),
fetch: vi.fn().mockReturnValue('Mocked fetch')
}))
};
});
test('utilisation d\'une classe mockée', () => {
const dog = new Dog('Rex');
expect(Dog).toHaveBeenCalledWith('Rex');
expect(dog.speak()).toBe('Mocked woof!');
expect(dog.fetch()).toBe('Mocked fetch');
});

Mock des méthodes statiques

Pour mocker des méthodes statiques, vous pouvez les définir directement sur la fonction constructeur mockée :

animal.js
export class Animal {
static getTypes() {
// Appel API complexe...
return ['dog', 'cat', 'bird'];
}
}
// animal.test.js
import { expect, test, vi } from 'vitest';
import { Animal } from './animal';
vi.mock('./animal', () => {
const Animal = vi.fn();
// Méthodes statiques sur le constructeur
Animal.getTypes = vi.fn().mockReturnValue(['mockDog', 'mockCat']);
return { Animal };
});
test('mock de méthodes statiques', () => {
expect(Animal.getTypes()).toEqual(['mockDog', 'mockCat']);
expect(Animal.getTypes).toHaveBeenCalled();
});

Utilisation Avancée de vi.spyOn()

La méthode vi.spyOn() est puissante pour surveiller et mocker des méthodes spécifiques sans remplacer l’objet entier.

Espionner les propriétés (getters/setters)

import { expect, test, vi } from 'vitest';
const user = {
get fullName() {
return 'John Doe';
},
set age(value) {
this._age = value;
},
get age() {
return this._age;
}
};
test('spy sur getters et setters', () => {
// Espionner un getter
const nameSpy = vi.spyOn(user, 'fullName', 'get');
expect(user.fullName).toBe('John Doe');
expect(nameSpy).toHaveBeenCalled();
// Mocker un getter
nameSpy.mockReturnValue('Jane Smith');
expect(user.fullName).toBe('Jane Smith');
// Espionner un setter
const ageSpy = vi.spyOn(user, 'age', 'set');
user.age = 30;
expect(ageSpy).toHaveBeenCalledWith(30);
// Restaurer tous les spies
vi.restoreAllMocks();
});

Espionner des méthodes d’objets imbriqués

import { expect, test, vi } from 'vitest';
const api = {
users: {
getAll: () => ['user1', 'user2'],
getById: (id) => ({ id, name: `User ${id}` })
}
};
test('spy sur des objets imbriqués', () => {
const getAllSpy = vi.spyOn(api.users, 'getAll');
const getByIdSpy = vi.spyOn(api.users, 'getById');
getAllSpy.mockReturnValue(['mockedUser1']);
getByIdSpy.mockImplementation((id) => ({ id, name: `Mocked ${id}` }));
expect(api.users.getAll()).toEqual(['mockedUser1']);
expect(api.users.getById(5)).toEqual({ id: 5, name: 'Mocked 5' });
// Vérification du nombre d'appels
expect(getByIdSpy).toHaveBeenCalledOnce();
expect(getByIdSpy).toHaveBeenCalledWith(5);
});

Le Système mocks

Vitest supporte le dossier __mocks__ pour organiser les mocks de modules externes et internes. Cette approche est particulièrement utile pour les modules réutilisés à travers plusieurs tests.

Structure du projet

├── src/
│ ├── utils/
│ │ └── api.js
│ ├── __mocks__/ // Mocks pour modules externes
│ │ └── axios.js
│ └── utils/
│ ├── __mocks__/ // Mocks pour modules internes
│ │ └── api.js
├── tests/

Exemple de mock externe

src/__mocks__/axios.js
export default {
get: vi.fn(() => Promise.resolve({ data: {} })),
post: vi.fn(() => Promise.resolve({ data: {} })),
put: vi.fn(() => Promise.resolve({ data: {} })),
delete: vi.fn(() => Promise.resolve({ data: {} }))
};
// test.js
import axios from 'axios';
import { vi } from 'vitest';
vi.mock('axios'); // Utilise automatiquement le fichier dans __mocks__/axios.js
test('mock d\'axios', async () => {
axios.get.mockResolvedValueOnce({ data: { users: ['user1'] } });
const response = await axios.get('/api/users');
expect(response.data.users).toEqual(['user1']);
});

Exemple de mock interne

src/utils/api.js
export const fetchUsers = async () => {
// Logique réelle...
};
// src/utils/__mocks__/api.js
export const fetchUsers = vi.fn(() =>
Promise.resolve(['mockedUser1', 'mockedUser2'])
);
// test.js
import { fetchUsers } from '../src/utils/api';
import { vi } from 'vitest';
vi.mock('../src/utils/api'); // Utilise automatiquement src/utils/__mocks__/api.js
test('mock d\'un module interne', async () => {
const users = await fetchUsers();
expect(users).toEqual(['mockedUser1', 'mockedUser2']);
});

Automocking Algorithm

Quand vous utilisez vi.mock() sans fournir de factory ou de fichier __mocks__, Vitest utilise un algorithme d’automocking intelligent :

import { vi } from 'vitest';
import { complexModule } from './complex';
// Sans factory, Vitest mocke automatiquement toutes les exports
vi.mock('./complex');
// L'algorithme d'automocking applique ces règles :
// - Les tableaux sont vidés
// - Les primitives et les collections restent identiques
// - Les objets sont clonés en profondeur
// - Les instances de classes sont mockées avec leurs prototypes

Exemple d’automocking

helpers.js
export const formatDate = (date) => {
// Logique complexe...
return date.toISOString();
};
export const API_URL = 'https://api.example.com';
export const config = {
timeout: 5000,
retries: 3
};
// test.js
import { formatDate, API_URL, config } from './helpers';
import { vi, expect, test } from 'vitest';
vi.mock('./helpers'); // Automocking
test('automocking', () => {
// Les fonctions sont automatiquement transformées en mocks
expect(vi.isMockFunction(formatDate)).toBe(true);
// Les primitives restent identiques
expect(API_URL).toBe('https://api.example.com');
// Les objets sont clonés en profondeur
expect(config).toEqual({ timeout: 5000, retries: 3 });
// Nous pouvons configurer le comportement des fonctions mockées
formatDate.mockReturnValue('2023-01-01');
expect(formatDate(new Date())).toBe('2023-01-01');
});

Conseils et Bonnes Pratiques

  1. Réinitialiser les mocks après chaque test

    afterEach(() => {
    vi.resetAllMocks();
    // ou
    vi.restoreAllMocks();
    });
  2. Importation des mocks dans le bon ordre Les appels à vi.mock() sont “hoistés” (déplacés en haut du fichier), donc ils sont exécutés avant les imports. Assurez-vous que votre logique en tient compte.

  3. Combiner mocking partiel et importation réelle

    vi.mock('./module', async (importOriginal) => {
    const original = await importOriginal();
    return {
    ...original, // Garde toutes les fonctions originales
    specificFunction: vi.fn() // Sauf celle-ci qui est mockée
    };
    });
  4. Vérifier la structure des appels

    const mockFn = vi.fn();
    mockFn('a', { complex: 'object' });
    // Vérification précise des appels
    expect(mockFn).toHaveBeenCalledWith('a', expect.objectContaining({ complex: expect.any(String) }));
    // Accès aux arguments d'appel
    expect(mockFn.mock.calls[0][1].complex).toBe('object');
  5. Mocker le temps avec vi.useFakeTimers()

    vi.useFakeTimers();
    const now = new Date(2023, 0, 1);
    vi.setSystemTime(now);
    // Toutes les opérations basées sur Date.now() utiliseront cette date
    expect(new Date().getFullYear()).toBe(2023);
    // N'oubliez pas de restaurer
    vi.useRealTimers();

En maîtrisant ces techniques avancées de mocking, vous pourrez écrire des tests plus précis, plus fiables et plus maintenables avec Vitest.