Aller au contenu principal
Version: 25.x

Simulations de classe ES6

Jest peut être utilisé pour simuler les classes ES6 qui sont importées dans les fichiers que vous voulez tester.

Les classes ES6 sont des fonctions constructrices avec du sucre syntaxique. Par conséquent, toute simulation d'une classe ES6 doit être une fonction ou une classe ES6 réelle (qui est, elle aussi, une autre fonction). Vous pouvez donc les simuler en utilisant des fonctions simulées.

Un exemple de classe ES6#

Nous utiliserons un exemple factice d'une classe qui lit des fichiers sonores, SoundPlayer, et une classe de consommatrice qui utilise cette classe, SoundPlayerConsumer. Nous allons simuler SoundPlayer dans nos tests pour SoundPlayerConsumer.

sound-player.js
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
}
sound-player-consumer.js
import SoundPlayer from './sound-player';
export default class SoundPlayerConsumer {
constructor() {
this.soundPlayer = new SoundPlayer();
}
playSomethingCool() {
const coolSoundFileName = 'song.mp3';
this.soundPlayer.playSoundFile(coolSoundFileName);
}
}

Les 4 façons de créer une simulation de classe ES6#

Simulation automatique#

L'appel à jest.mock('./sound-player') renvoie une « simulation automatique » utile que vous pouvez utiliser pour espionner les appels au constructeur de la classe et à toutes ses méthodes. Elle remplace la classe ES6 par un constructeur simulé, et remplace toutes ses méthodes par des fonctions simulées qui renvoient toujours undefined. Les appels de méthode sont enregistrés dans theAutomaticMock.mock.instances[index].methodName.mock.calls.

Veuillez noter que si vous utilisez des fonctions fléchées dans vos classes, elles ne feront pas partie de la simulation. La raison, c'est que les fonctions fléchées ne sont pas présentes sur le prototype de l'objet, ce sont simplement des propriétés contenant une référence à une fonction.

Si vous n'avez pas besoin de remplacer l'implémentation de la classe, c'est l'option la plus simple à mettre en place. Par exemple :

import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer est maintenant un constructeur simulé
beforeEach(() => {
// Efface toutes les instances et les appels au constructeur et à toutes les méthodes :
SoundPlayer.mockClear();
});
it('Nous pouvons vérifier si le consommateur a appelé le constructeur de la classe', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it('Nous pouvons vérifier si le consommateur a appelé une méthode sur l'instance de la classe', () => {
// Montre que mockClear() fonctionne :
expect(SoundPlayer).not.toHaveBeenCalled();
const soundPlayerConsumer = new SoundPlayerConsumer();
// Le constructeur devrait être appelé à nouveau :
expect(SoundPlayer).toHaveBeenCalledTimes(1);
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
// mock.instances est disponible avec les simulations automatiques :
const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
// Équivalent au contrôle ci-dessus :
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});

Simulation manuelle#

Créez une simulation manuelle en sauvegardant une implémentation de simulation dans le dossier __mocks__. Cela vous permet de spécifier l'implémentation, et cela peut être utilisé dans plusieurs fichiers de test.

__mocks__/sound-player.js
// Import this named export into your test file:
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;

Importez la simulation et la méthode simulée partagée par toutes les instances :

sound-player-consumer.test.js
import SoundPlayer, {mockPlaySoundFile} from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
beforeEach(() => {
// Clear all instances and calls to constructor and all methods:
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
it('We can check if the consumer called the class constructor', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it('We can check if the consumer called a method on the class instance', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
});

Appel de jest.mock() avec le paramètre factory du module#

jest.mock(path, moduleFactory) prend un argument factory de module. Un factory de module est une fonction qui renvoie la simulation.

Pour pouvoir simuler une fonction de constructeur, la factory de module doit retourner une fonction de constructeur. En d'autres termes, le factory de module doit être une fonction qui renvoie une fonction - une fonction d'ordre supérieur (« higher-order function » HOF).

import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});

Une limitation avec le paramètre factory est que, puisque les appels à jest.mock() sont remontés en haut du fichier, il n'est pas possible de définir d'abord une variable et ensuite de l'utiliser dans la factory. Une exception est faite pour les variables qui commencent par le mot 'mock'. C'est à vous de garantir qu'elles seront initialisées à temps ! Par exemple, l'exemple suivant déclenchera une erreur hors du champ d'application en raison de l'utilisation de 'fake' au lieu de 'mock' dans la déclaration de la variable :

// Remarque : ceci échouera
import SoundPlayer from './sound-player';
const fakePlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: fakePlaySoundFile};
});
});

Remplacer la simulation en utilisant mockImplementation() ou mockImplementationOnce()#

Vous pouvez remplacer tous les simulations ci-dessus afin de modifier l'implémentation, pour un seul test ou tous les tests, en appelant mockImplementation() sur la simulation existante.

Les appels à jest.mock sont remontés au sommet du code. Vous pouvez spécifier une simulation ultérieurement, par exemple dans beforeAll(), en appelant mockImplementation() (ou mockImplementationOnce()) sur la simulation existante au lieu d'utiliser le paramètre factory. Cela vous permet également de modifier la simulation entre les tests, si nécessaire :

import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player');
describe('Quand SoundPlayer lève une erreur', () => {
beforeAll(() => {
SoundPlayer.mockImplementation(() => {
return {
playSoundFile: () => {
throw new Error('Erreur de test');
},
};
});
});
it('Il devrait y avoir une erreur lors de l\'appel de playSomethingCool', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
});
});

En détail : comprendre les fonctions de constructeur simulées#

La construction de votre fonction constructeur simulée en utilisant jest.fn().mockImplementation() fait croire que les simulations sont plus compliquées qu'elles ne le sont réellement. Cette section montre comment vous pouvez créer vos propres simulations pour illustrer le fonctionnement de la simulation.

Simulation manuelle qui est une autre classe ES6#

Si vous définissez une classe ES6 utilisant le même nom de fichier que la classe simulée dans le dossier __mocks__, elle servira de simulation. Cette classe sera utilisée à la place de la classe réelle. Cela vous permet d'injecter une implémentation de test pour la classe, mais ne fournit pas un moyen d'espionner les appels.

Pour l'exemple inventé, la simulation pourrait ressembler à ceci :

__mocks__/sound-player.js
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
}
playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
}
}

Simuler en utilisant le paramètre factory du module#

La fonction factory de modules passée à jest.mock(path, moduleFactory) peut être un HOF qui renvoie une fonction*. Cela permettra d'appeler new sur la simulation. Encore une fois, cela vous permet d'injecter un comportement différent pour les tests, mais ne fournit pas un moyen d'espionner les appels.

* La fonction factory du module doit retourner une fonction#

Pour pouvoir simuler une fonction de constructeur, la factory de module doit retourner une fonction de constructeur. En d'autres termes, le factory de module doit être une fonction qui renvoie une fonction - une fonction d'ordre supérieur (« higher-order function » HOF).

jest.mock('./sound-player', () => {
return function () {
return {playSoundFile: () => {}};
};
});

Remarque : les fonctions fléchées ne fonctionneront pas

Remarquez que la simulation ne peut pas être une fonction fléchée car l'appel à new sur une fonction fléchée n'est pas autorisé en JavaScript. Donc ça ne marchera pas :

jest.mock('./sound-player', () => {
return () => {
// Ne fonctionne pas; les fonctions fléchées ne peuvent pas être appelées avec new
return {playSoundFile: () => {}};
};
});

Cela lèvera TypeError: _soundPlayer2.default is not a constructor, sauf si le code est transposé en ES5, par exemple par @babel/preset-env. (ES5 n'a pas de fonctions fléchées ni de classes, donc les deux seront transpiilées en fonctions simples.)

Suivre l'utilisation (espionner la simulation)#

L'injection d'une implémentation de test est utile, mais vous voudrez probablement aussi tester si le constructeur et les méthodes de la classe sont appelés avec les bons paramètres.

Espionnage du constructeur#

Afin de suivre les appels au constructeur, remplacez la fonction retournée par le HOF par une fonction simulée Jest. Créez-la avec jest.fn(), puis spécifiez son implémentation avec mockImplementation().

import SoundPlayer from './sound-player';
jest.mock('./sound-player', () => {
// Fonctionne et vous permet de vérifier les appels du constructeur :
return jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
});
});

Cela nous permettra d'inspecter l'utilisation de notre classe simulée, en utilisant SoundPlayer.mock.calls : expect(SoundPlayer).toHaveBeenCalled(); ou un équivalent proche : expect(SoundPlayer.mock.calls.length).toEqual(1);

Simulation de classe non exportée par défaut#

Si la classe n'est pas l'exportation par défaut du module alors vous devez retourner un objet avec la clé qui est la même que le nom d'exportation de la classe.

import {SoundPlayer} from './sound-player';
jest.mock('./sound-player', () => {
// Fonctionne et vous permet de vérifier les appels du constructeur :
return {
SoundPlayer: jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
}),
};
});

Espionnage sur les méthodes de notre classe#

Notre classe simulée devra fournir toutes les fonctions membres (playSoundFile dans l'exemple) qui seront appelées pendant nos tests, sinon nous obtiendrons une erreur pour avoir appelé une fonction qui n'existe pas. Mais nous voudrons probablement aussi espionner les appels à ces méthodes, pour nous assurer qu'elles ont été appelées avec les paramètres attendus.

Un nouvel objet sera créé chaque fois que la fonction constructeur simulée sera appelée pendant les tests. Pour espionner les appels de méthode dans tous ces objets, nous remplissons playSoundFile avec une autre fonction simulée, et nous stockons une référence à cette même fonction simulée dans notre fichier de test, afin qu'elle soit disponible pendant les tests.

import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
// Nous pouvons maintenant suivre les appels vers playSoundFile
});
});

L'équivalent de la simulation manuelle serait le suivant :

__mocks__/sound-player.js
// Import this named export into your test file
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;

L'utilisation est similaire à la fonction factory du module, sauf que vous pouvez omettre le second argument de jest.mock(), et vous devez importer la méthode simulée dans votre fichier de test, puisqu'elle n'y est plus définie. Utilisez le chemin du module original pour cela ; n'incluez pas __mocks__.

Nettoyage entre les tests#

Pour effacer l'enregistrement des appels à la fonction constructeur simulée et à ses méthodes, nous appelons mockClear() dans la fonction beforeEach() :

beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});

Exemple complet#

Voici un fichier de test complet qui utilise le paramètre factory du module pour jest.mock :

sound-player-consumer.test.js
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
it('The consumer should be able to call new() on SoundPlayer', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
// Ensure constructor created the object:
expect(soundPlayerConsumer).toBeTruthy();
});
it('We can check if the consumer called the class constructor', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it('We can check if the consumer called a method on the class instance', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
});