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.
export class Dog { constructor(name) { this.name = name; }
speak() { return `${this.name} says woof!`; }
fetch() { // logique complexe... return "Got the ball!"; }}
// dog.test.jsimport { 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 :
export class Animal { static getTypes() { // Appel API complexe... return ['dog', 'cat', 'bird']; }}
// animal.test.jsimport { 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
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.jsimport 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
export const fetchUsers = async () => { // Logique réelle...};
// src/utils/__mocks__/api.jsexport const fetchUsers = vi.fn(() => Promise.resolve(['mockedUser1', 'mockedUser2']));
// test.jsimport { 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 exportsvi.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
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.jsimport { 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
-
Réinitialiser les mocks après chaque test
afterEach(() => {vi.resetAllMocks();// ouvi.restoreAllMocks();}); -
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. -
Combiner mocking partiel et importation réelle
vi.mock('./module', async (importOriginal) => {const original = await importOriginal();return {...original, // Garde toutes les fonctions originalesspecificFunction: vi.fn() // Sauf celle-ci qui est mockée};}); -
Vérifier la structure des appels
const mockFn = vi.fn();mockFn('a', { complex: 'object' });// Vérification précise des appelsexpect(mockFn).toHaveBeenCalledWith('a', expect.objectContaining({ complex: expect.any(String) }));// Accès aux arguments d'appelexpect(mockFn.mock.calls[0][1].complex).toBe('object'); -
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 dateexpect(new Date().getFullYear()).toBe(2023);// N'oubliez pas de restaurervi.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.