Aller au contenu principal
Version : 28.0

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('Lecture du fichier audio ' + 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
// Importe cet export nommé dans votre fichier de test :
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 est maintenant un constructeur simulé

beforeEach(() => {
// Efface toutes les instances et les appels au constructeur et à toutes les méthodes :
SoundPlayer.mockClear();
mockPlaySoundFile.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', () => {
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};
});
});
attention

Comme les appels à jest.mock() sont remontés au sommet du fichier, Jest empêche l'accès aux variables hors de portée. Par défaut, vous ne pouvez pas d'abord définir une variable et ensuite l'utiliser dans la factory. Jest désactivera cette vérification pour les variables qui commencent par le mot mock. Cependant, c'est à vous de garantir qu'elles seront initialisées à temps . Attention à la zone morte temporaire.

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};
});
});

Ce qui suit lancera une ReferenceError malgré l'utilisation de mock dans la déclaration de la variable, car le mockSoundPlayer n'est pas enveloppé dans une fonction fléchée et elle est donc accédée avant l'initialisation après le hissage.

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

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 : le constructeur a été appelé');
}

playSoundFile() {
console.log('Mock SoundPlayer : playSoundFile a été appelé');
}
}

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.)

Simulation d'une méthode spécifique d'une classe

Supposons que vous voulez simuler ou espionner la méthode playSoundFile dans la classe SoundPlayer. Un exemple simple :

// votre fichier jest ci-dessous
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';

const playSoundFileMock = jest
.spyOn(SoundPlayer.prototype, 'playSoundFile')
.mockImplementation(() => {
console.log('fonction simulée');
}); // commentez cette ligne si vous voulez juste « espionner ».

it('le lecteur joue de la musique', () => {
const player = new SoundPlayerConsumer();
player.playSomethingCool();
expect(playSoundFileMock).toHaveBeenCalled();
});

Méthodes statiques, getter et setter

Imaginons que notre classe SoundPlayer a une méthode getter foo et une méthode statique brand

export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}

playSoundFile(fileName) {
console.log('Lecture du fichier audio ' + fileName);
}

get foo() {
return 'bar';
}
static brand() {
return 'player-brand';
}
}

Vous pouvez les simulerer/espionner facilement, voici un exemple :

// votre fichier de test jest ci-dessous
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';

const staticMethodMock = jest
.spyOn(SoundPlayer, 'brand')
.mockImplementation(() => 'some-mocked-brand');

const getterMethodMock = jest
.spyOn(SoundPlayer.prototype, 'foo', 'get')
.mockImplementation(() => 'some-mocked-result');

it('les méthodes personnalisées sont appelées', () => {
const player = new SoundPlayer();
const foo = player.foo;
const brand = SoundPlayer.brand();

expect(staticMethodMock).toHaveBeenCalled();
expect(getterMethodMock).toHaveBeenCalled();
});

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
// Importe cet export nommé dans votre fichier de test
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('Le consommateur doit pouvoir appeler new() sur SoundPlayer', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
// Ensure constructor created the object:
expect(soundPlayerConsumer).toBeTruthy();
});

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', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
});