Aller au contenu principal
Version : 29.7

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). So you can mock them using mock functions.

Un exemple de classe ES6

We'll use a contrived example of a class that plays sound files, SoundPlayer, and a consumer class which uses that class, SoundPlayerConsumer. We'll mock SoundPlayer in our tests for 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

Calling jest.mock('./sound-player') returns a useful "automatic mock" you can use to spy on calls to the class constructor and all of its methods. It replaces the ES6 class with a mock constructor, and replaces all of its methods with mock functions that always return undefined. Method calls are saved in theAutomaticMock.mock.instances[index].methodName.mock.calls.

remarque

If you use arrow functions in your classes, they will not be part of the mock. 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 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

Create a manual mock by saving a mock implementation in the __mocks__ folder. 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);
});

Calling jest.mock() with the module factory parameter

jest.mock(path, moduleFactory) takes a module factory argument. 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

Since calls to jest.mock() are hoisted to the top of the file, Jest prevents access to out-of-scope variables. Par défaut, vous ne pouvez pas d'abord définir une variable et ensuite l'utiliser dans la factory. Jest will disable this check for variables that start with the word mock. Cependant, c'est à vous de garantir qu'elles seront initialisées à temps . Be aware of Temporal Dead Zone.

For example, the following will throw an out-of-scope error due to the use of fake instead of mock in the variable declaration.

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

The following will throw a ReferenceError despite using mock in the variable declaration, as the mockSoundPlayer is not wrapped in an arrow function and thus accessed before initialization after hoisting.

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

Replacing the mock using mockImplementation() or mockImplementationOnce()

You can replace all of the above mocks in order to change the implementation, for a single test or all tests, by calling mockImplementation() on the existing mock.

Les appels à jest.mock sont remontés au sommet du code. You can specify a mock later, e.g. in beforeAll(), by calling mockImplementation() (or mockImplementationOnce()) on the existing mock instead of using the factory parameter. 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('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();
});
});

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

Building your constructor function mock using jest.fn().mockImplementation() makes mocks appear more complicated than they really are. 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

If you define an ES6 class using the same filename as the mocked class in the __mocks__ folder, it will serve as the mock. 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

The module factory function passed to jest.mock(path, moduleFactory) can be a HOF that returns a function*. This will allow calling new on the mock. 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

The mock can't be an arrow function because calling new on an arrow function is not allowed in JavaScript. Donc ça ne marchera pas :

jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions can't be called with new
return {playSoundFile: () => {}};
};
});

This will throw TypeError: _soundPlayer2.default is not a constructor, unless the code is transpiled to ES5, e.g. by @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

Lets say that you want to mock or spy on the method playSoundFile within the class SoundPlayer. Un 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, getter et setter

Lets imagine our class SoundPlayer has a getter method foo and a static method 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';
}
}

You can mock/spy on them easily, here is an example:

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

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. Create it with jest.fn(), and then specify its implementation with mockImplementation().

import SoundPlayer from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
});
});

This will let us inspect usage of our mocked class, using SoundPlayer.mock.calls: expect(SoundPlayer).toHaveBeenCalled(); or near-equivalent: expect(SoundPlayer.mock.calls.length).toBeGreaterThan(0);

Simulation de classe non exportée par défaut

If the class is not the default export from the module then you need to return an object with the key that is the same as the class export name.

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 sur les méthodes de notre classe

Our mocked class will need to provide any member functions (playSoundFile in the example) that will be called during our tests, or else we'll get an error for calling a function that doesn't exist. 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. To spy on method calls in all of these objects, we populate playSoundFile with another mock function, and store a reference to that same mock function in our test file, so it's available during 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 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;

Usage is similar to the module factory function, except that you can omit the second argument from jest.mock(), and you must import the mocked method into your test file, since it is no longer defined there. Use the original module path for this; don't include __mocks__.

Nettoyage entre les tests

To clear the record of calls to the mock constructor function and its methods, we call mockClear() in the beforeEach() function:

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

Exemple complet

Here's a complete test file which uses the module factory parameter to 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]).toBe(coolSoundFileName);
});