Mocks de classes ES6
Cette page a été traduite par PageTurner AI (bêta). Non approuvée officiellement par le projet. Vous avez trouvé une erreur ? Signaler un problème →
Jest peut être utilisé pour simuler des classes ES6 importées dans les fichiers que vous souhaitez tester.
Les classes ES6 sont des fonctions constructeur avec du sucre syntaxique. Par conséquent, toute simulation d'une classe ES6 doit être une fonction ou une véritable classe ES6 (qui est elle-même une fonction). Vous pouvez donc les simuler en utilisant des fonctions simulées.
Un exemple de classe ES6
Nous utiliserons un exemple artificiel d'une classe lisant des fichiers audio, SoundPlayer, et d'une classe consommatrice utilisant cette classe, SoundPlayerConsumer. Nous simulerons SoundPlayer dans nos tests pour SoundPlayerConsumer.
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
}
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 méthodes pour créer une simulation de classe ES6
Simulation automatique
L'appel à jest.mock('./sound-player') retourne une "simulation automatique" utile pour espionner les appels au constructeur de classe et toutes ses méthodes. Elle remplace la classe ES6 par un constructeur simulé et toutes ses méthodes par des fonctions simulées retournant toujours undefined. Les appels de méthode sont enregistrés dans theAutomaticMock.mock.instances[index].methodName.mock.calls.
Si vous utilisez des fonctions fléchées dans vos classes, elles ne feront pas partie de la simulation. La raison 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 référençant une fonction.
Si vous n'avez pas besoin de remplacer l'implémentation de la classe, c'est l'option la plus simple à configurer. Par exemple :
import SoundPlayer 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();
});
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', () => {
// Show that mockClear() is working:
expect(SoundPlayer).not.toHaveBeenCalled();
const soundPlayerConsumer = new SoundPlayerConsumer();
// Constructor should have been called again:
expect(SoundPlayer).toHaveBeenCalledTimes(1);
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
// mock.instances is available with automatic mocks:
const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
expect(mockPlaySoundFile.mock.calls[0][0]).toBe(coolSoundFileName);
// Equivalent to above check:
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});
Simulation manuelle
Créez une simulation manuelle en sauvegardant une implémentation simulée dans le dossier __mocks__. Cela vous permet de spécifier l'implémentation, réutilisable dans plusieurs fichiers de test.
// 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 :
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 d'usine de module
jest.mock(path, moduleFactory) prend un argument d'usine de module. Une usine de module est une fonction qui retourne la simulation.
Pour simuler une fonction de construction, le module factory doit retourner une fonction constructeur. Autrement dit, le module factory doit être une fonction qui retourne une fonction - une fonction d'ordre supérieur (HOF).
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
Les appels à jest.mock() étant remontés en haut du fichier, Jest empêche l'accès aux variables hors de portée. Par défaut, vous ne pouvez pas définir une variable puis l'utiliser dans l'usine. Jest désactive cette vérification pour les variables commençant par mock. Cependant, vous devez garantir leur initialisation à temps. Tenez compte de la Zone morte temporelle (Temporal Dead Zone).
Par exemple, le code suivant générera une erreur hors de portée à cause de l'utilisation de fake au lieu de mock dans la déclaration de variable.
// Note: this will fail
import SoundPlayer from './sound-player';
const fakePlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: fakePlaySoundFile};
});
});
Le code suivant générera une ReferenceError malgré l'utilisation de mock, car mockSoundPlayer n'est pas encapsulé dans une fonction fléchée et est donc accédé avant initialisation après remontée.
import SoundPlayer from './sound-player';
const mockSoundPlayer = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
// results in a ReferenceError
jest.mock('./sound-player', () => {
return mockSoundPlayer;
});
Remplacement de la simulation avec mockImplementation() ou mockImplementationOnce()
Vous pouvez remplacer toutes les simulations ci-dessus pour modifier l'implémentation, pour un test unique ou tous les tests, en appelant mockImplementation() sur la simulation existante.
Les appels à jest.mock sont remontés en haut du code. Vous pouvez spécifier un mock ultérieurement, par exemple dans beforeAll(), en appelant mockImplementation() (ou mockImplementationOnce()) sur le mock existant au lieu d'utiliser le paramètre factory. Cela vous permet également de modifier le mock entre les tests si nécessaire :
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player');
describe('When SoundPlayer throws an error', () => {
beforeAll(() => {
SoundPlayer.mockImplementation(() => {
return {
playSoundFile: () => {
throw new Error('Test error');
},
};
});
});
it('Should throw an error when calling playSomethingCool', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
});
});
Approfondissement : Comprendre les fonctions de construction de mocks
Construire votre fonction de construction mock avec jest.fn().mockImplementation() peut donner une impression de complexité artificielle. Cette section montre comment créer vos propres mocks pour illustrer leur fonctionnement réel.
Mock manuel sous forme d'une autre classe ES6
Si vous définissez une classe ES6 dans le dossier __mocks__ avec le même nom de fichier que la classe à simuler, elle servira de mock. Cette classe remplacera la classe réelle. Cela permet d'injecter une implémentation de test pour la classe, mais sans possibilité d'espionner les appels.
Pour notre exemple artificiel, le mock pourrait ressembler à ceci :
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
}
playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
}
}
Mock utilisant le paramètre module factory
La fonction factory passée à jest.mock(path, moduleFactory) peut être une fonction d'ordre supérieur (HOF) renvoyant une fonction*. Cela permettra d'utiliser new sur le mock. Là encore, cela permet d'injecter différents comportements pour les tests, mais sans espionnage des appels.
* La fonction factory doit renvoyer une fonction
Pour simuler une fonction de construction, le module factory doit retourner une fonction constructeur. Autrement dit, le module factory doit être une fonction qui retourne une fonction - une fonction d'ordre supérieur (HOF).
jest.mock('./sound-player', () => {
return function () {
return {playSoundFile: () => {}};
};
});
Le mock ne peut pas être une fonction fléchée car l'appel à new sur une fonction fléchée est interdit en JavaScript. Ceci ne fonctionnera donc pas :
jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions can't be called with new
return {playSoundFile: () => {}};
};
});
Cela générera une TypeError: _soundPlayer2.default is not a constructor, sauf si le code est transpilé en ES5 (par exemple avec @babel/preset-env). (L'ES5 n'a ni fonctions fléchées ni classes, les deux seront transpilés en fonctions standards.)
Simuler une méthode spécifique d'une classe
Supposons que vous souhaitiez simuler ou espionner la méthode playSoundFile de la classe SoundPlayer. Exemple simple :
// your jest test file below
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
const playSoundFileMock = jest
.spyOn(SoundPlayer.prototype, 'playSoundFile')
.mockImplementation(() => {
console.log('mocked function');
}); // comment this line if just want to "spy"
it('player consumer plays music', () => {
const player = new SoundPlayerConsumer();
player.playSomethingCool();
expect(playSoundFileMock).toHaveBeenCalled();
});
Méthodes statiques, getters et setters
Imaginons que notre classe SoundPlayer ait un getter foo et une méthode statique brand
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
get foo() {
return 'bar';
}
static brand() {
return 'player-brand';
}
}
Vous pouvez facilement les simuler/espionner, voici un exemple :
// your jest test file below
import SoundPlayer from './sound-player';
const staticMethodMock = jest
.spyOn(SoundPlayer, 'brand')
.mockImplementation(() => 'some-mocked-brand');
const getterMethodMock = jest
.spyOn(SoundPlayer.prototype, 'foo', 'get')
.mockImplementation(() => 'some-mocked-result');
it('custom methods are called', () => {
const player = new SoundPlayer();
const foo = player.foo;
const brand = SoundPlayer.brand();
expect(staticMethodMock).toHaveBeenCalled();
expect(getterMethodMock).toHaveBeenCalled();
});
Suivi de l'utilisation (espionnage du mock)
Injecter une implémentation de test est utile, mais vous voudrez probablement aussi vérifier si le constructeur et les méthodes de la classe sont appelés avec les bons paramètres.
Espionnage du constructeur
Pour suivre les appels au constructeur, remplacez la fonction renvoyée par la HOF par une fonction mock de Jest. Créez-la avec jest.fn(), puis spécifiez son implémentation avec mockImplementation().
import SoundPlayer from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
});
});
Cela nous permettra d'inspecter l'utilisation de notre classe mockée via SoundPlayer.mock.calls : expect(SoundPlayer).toHaveBeenCalled(); ou équivalent : expect(SoundPlayer.mock.calls.length).toBeGreaterThan(0);
Simulation de classes non exportées par défaut
Si la classe n'est pas l'export par défaut du module, vous devez retourner un objet avec une clé correspondant au nom d'export de la classe.
import {SoundPlayer} from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return {
SoundPlayer: jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
}),
};
});
Espionnage des méthodes de notre classe
Notre classe mockée devra fournir toutes les fonctions membres (playSoundFile dans l'exemple) qui seront appelées pendant nos tests, sinon nous obtiendrons une erreur pour l'appel d'une fonction inexistante. Mais nous voudrons probablement aussi espionner les appels à ces méthodes, pour vérifier qu'elles ont été appelées avec les paramètres attendus.
Un nouvel objet sera créé à chaque appel de la fonction constructeur mockée pendant les tests. Pour espionner les appels de méthode dans tous ces objets, nous assignons une autre fonction mock à playSoundFile, et stockons une référence à cette même fonction mock 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};
// Now we can track calls to playSoundFile
});
});
L'équivalent manuel de ce mock serait :
// 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 de module, sauf que vous pouvez omettre le second argument de jest.mock(), et vous devez importer la méthode mockée dans votre fichier de test, car 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'historique des appels au constructeur mocké et à ses méthodes, nous appelons mockClear() dans la fonction beforeEach() :
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
Exemple complet
Voici un fichier de test complet utilisant le paramètre factory de module pour jest.mock :
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]).toBe(coolSoundFileName);
});