Aller au contenu principal
Version: 25.x

Fonctions simulées

Les fonctions simulées permettent de tester les liens entre le code en effaçant l'implémentation réelle d'une fonction, en capturant les appels à la fonction (et les paramètres passés dans ces appels), en capturant les instances des fonctions constructrices lorsqu'elles sont instanciées avec new, et en permettant la configuration des valeurs de retour au moment du test.

Il existe deux façons de simuler des fonctions : soit en créant une fonction simulée à utiliser dans le code de test, soit en écrivant une simulation manuelle pour remplacer une dépendance du module.

Utilisation d'une fonction simulée#

Imaginons que nous testons l'implémentation d'une fonction forEach, qui appelle un callback pour chaque élément d'un tableau fourni.

function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}

Pour tester cette fonction, nous pouvons utiliser une fonction simulée, et inspecter l'état de la fonction simulée pour nous assurer que le callback est appelé comme prévu.

const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
// La fonction simulée est appelée deux fois
expect(mockCallback.mock.calls.length).toBe(2);
// Le premier argument du premier appel à la fonction était 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// Le premier argument du deuxième appel à la fonction était 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// La valeur de retour du premier appel à la fonction était 42
expect(mockCallback.mock.results[0].value).toBe(42);

Propriété .mock#

Toutes les fonctions simulées ont cette propriété spéciale .mock, qui est l'endroit où sont conservées les données sur la façon dont la fonction a été appelée et ce que la fonction a renvoyé. La propriété .mock trace également la valeur de this pour chaque appel, il est donc possible de l'inspecter également :

const myMock = jest.fn();
const a = new myMock();
const b = {};
const bound = myMock.bind(b);
bound();
console.log(myMock.mock.instances);
// > [ <a>, <b> ]

Ces membres simulés sont très utiles dans les tests pour vérifier comment ces fonctions sont appelées, instanciées, ou ce qu'elles retournent :

// La fonction a été appelée exactement une fois
expect(someMockFunction.mock.calls.length).toBe(1);
// Le premier argument du premier appel à la fonction était 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
// Le deuxième argument du premier appel à la fonction était 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
// La valeur de retour du premier appel à la fonction était 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value');
// Cette fonction a été instanciée exactement deux fois
expect(someMockFunction.mock.instances.length).toBe(2);
// L'objet retourné par la première instanciation de cette fonction
// avait une propriété `name` dont la valeur était définie à 'test'
expect(someMockFunction.mock.instances[0].name).toEqual('test');

Valeurs de retour simulées#

Les fonctions simulées peuvent également être utilisées pour injecter des valeurs de test dans votre code pendant un test :

const myMock = jest.fn();
console.log(myMock());
// > undefined
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

Les fonctions simulées sont également très efficaces dans le code qui utilise un « style de passage de continuation » (NdT continuation-passing style) fonctionnel. Le code écrit dans ce style permet d'éviter le recours à des blocs compliqués qui recréent le comportement du composant réel qu'ils remplacent, en faveur de l'injection de valeurs directement dans le test juste avant leur utilisation.

const filterTestFn = jest.fn();
// Faire en sorte que la simulation renvoie `true` pour le premier appel,
// et `false` pour le second appel
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);
const result = [11, 12].filter(num => filterTestFn(num));
console.log(result);
// > [11]
console.log(filterTestFn.mock.calls);
// > [ [11], [12] ]

La plupart des exemples réels impliquent en fait de saisir une fonction fictive sur un composant dépendant et de la configurer, mais la technique est la même. Dans ces cas, essayez d'éviter la tentation d'implémenter la logique à l'intérieur de toute fonction qui n'est pas directement testée.

Modules de simulation#

Supposons que nous ayons une classe qui récupère les utilisateurs de notre API. La classe utilise axios pour appeler l'API puis retourne l'attribut data qui contient tous les utilisateurs :

// users.js
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;

Maintenant, afin de tester cette méthode sans toucher à l'API (et donc créer des tests lents et fragiles), nous pouvons utiliser la fonction jest.mock(...) pour simuler automatiquement le module axios.

Une fois le module simulé, nous pouvons fournir un mockResolvedValue pour .get qui renvoie les données que nous voulons que notre test vérifie. En effet, nous voulons que axios.get('/users.json') renvoie une fausse réponse.

// users.test.js
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('doit récupérer les utilisateurs', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);
// ou vous pouvez utiliser ce qui suit en fonction de votre cas d'utilisation :
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then(data => expect(data).toEqual(users));
});

Simulation partielle#

Les sous-ensembles d'un module peuvent être simulés et le reste du module peut conserver son implémentation réelle :

// foo-bar-baz.js
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
//test.js
import defaultExport, {bar, foo} from '../foo-bar-baz';
jest.mock('../foo-bar-baz', () => {
const originalModule = jest.requireActual('../foo-bar-baz');
//Simule l'exportation par défaut et l'exportation nommée 'foo'
return {
__esModule: true,
...originalModule,
default: jest.fn(() => 'mocked baz'),
foo: 'mocked foo',
};
});
test('devrait faire une simulation partielle', () => {
const defaultExportResult = defaultExport();
expect(defaultExportResult).toBe('mocked baz');
expect(defaultExport).toHaveBeenCalled();
expect(foo).toBe('mocked foo');
expect(bar()).toBe('bar');
});

Implémentations simulées#

Pourtant, il existe des cas où il est utile d'aller plus loin que la possibilité de spécifier des valeurs de retour et de remplacer complètement l'implémentation d'une fonction simulée. Cela peut être fait avec jest.fn ou la méthode mockImplementationOnce sur les fonctions simulées.

const myMockFn = jest.fn(cb => cb(null, true));
myMockFn((err, val) => console.log(val));
// > true

La méthode mockImplementation est utile lorsque vous devez définir l'implémentation par défaut d'une fonction simulée qui est créée à partir d'un autre module :

// foo.js
module.exports = function () {
// une implementation;
};
// test.js
jest.mock('../foo'); // cela se produit automatiquement avec l'autosimulation
const foo = require('../foo');
// foo est une fonction simulée
foo.mockImplementation(() => 42);
foo();
// > 42

Lorsque vous devez recréer un comportement complexe d'une fonction simulée, de sorte que plusieurs appels de fonction produisent des résultats différents, utilisez la méthode mockImplementationOnce :

const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, false));
myMockFn((err, val) => console.log(val));
// > true
myMockFn((err, val) => console.log(val));
// > false

Lorsque la fonction simulée manque d'implémentations définies avec mockImplementationOnce, elle exécutera l'implémentation par défaut définie avec jest.fn (si elle est définie) :

const myMockFn = jest
.fn(() => 'par défaut')
.mockImplementationOnce(() => 'premier appel')
.mockImplementationOnce(() => 'second appel');
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'premier appel', 'second appel', 'par défaut', 'par défaut'

Pour les cas où nous avons des méthodes qui sont typiquement enchaînées (et donc doivent toujours retourner this), nous avons une API sucrée pour simplifier cela sous la forme d'une fonction .mockReturnThis() qui se trouve également sur tous les simulations :

const myObj = {
myMethod: jest.fn().mockReturnThis(),
};
// est identique à
const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};

Noms simulés#

Vous pouvez éventuellement fournir un nom pour vos fonctions simulées, qui sera affiché à la place de "jest.fn()" dans le résultat d'erreur du test. Utilisez cette option si vous voulez être en mesure d'identifier rapidement la fonction simulée qui signale une erreur dans le résultat de votre test.

const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(scalar => 42 + scalar)
.mockName('add42');

Comparateurs personnalisés#

Enfin, pour qu'il soit moins difficile de déterminer comment les fonctions simulées ont été appelées, nous avons ajouté quelques fonctions comparatrices personnalisées :

// La fonction simulée a été appelée au moins une fois
expect(mockFunc).toHaveBeenCalled();
// La fonction simulée a été appelée au moins une fois avec les arguments spécifiés
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
// Le dernier appel à la fonction simulée a été appelé avec les arguments spécifiés
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
// Tous les appels et le nom de la simulation sont écrits en tant que snapshot
expect(mockFunc).toMatchSnapshot();

Ces comparateurs sont du sucre pour les formes courantes d'inspection de la propriété .mock. Vous pouvez toujours le faire manuellement si cela vous convient mieux ou si vous avez besoin de quelque chose de plus spécifique :

// La fonction simulée a été appelée au moins une fois
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);
// La fonction simulée a été appelée au moins une fois avec les arguments spécifiés
expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);
// Le dernier appel à la fonction fantaisie a été appelé avec les arguments spécifiés
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
arg1,
arg2,
]);
// Le premier argument du dernier appel à la fonction simulée était `42`
// (notez qu'il n'existe pas d'aide sucrée pour cette assertion spécifique)
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);
// Un snapshot vérifiera qu'une simulation a été appelée le même nombre de fois,
// dans le même ordre, avec les mêmes arguments. Il vérifiera également le nom.
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.getMockName()).toBe('un nom de simulation');

Pour une liste complète des comparateurs, consultez les docs de référence.