Aller au contenu principal
Version : 29.7

Fonctions fictives (Mocks)

Traduction Bêta Non Officielle

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 →

Les fonctions fictives permettent de tester les interactions entre composants en remplaçant l'implémentation réelle d'une fonction. Elles capturent les appels à la fonction (avec les paramètres passés), les instances de fonctions constructeurs instanciées via new, et permettent de configurer des valeurs de retour pendant les tests.

Il existe deux méthodes pour créer des fonctions fictives : soit en créant une fonction fictive à utiliser dans le code de test, soit en écrivant un manual mock pour surcharger une dépendance module.

Utiliser une fonction fictive

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

forEach.js
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}

Pour tester cette fonction, nous pouvons utiliser une fonction fictive et examiner son état pour vérifier que le callback est bien appelé comme prévu.

forEach.test.js
const forEach = require('./forEach');

const mockCallback = jest.fn(x => 42 + x);

test('forEach mock function', () => {
forEach([0, 1], mockCallback);

// The mock function was called twice
expect(mockCallback.mock.calls).toHaveLength(2);

// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);

// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);
});

Propriété .mock

Toutes les fonctions fictives possèdent cette propriété spéciale .mock qui conserve les données sur la façon dont la fonction a été appelée et ce qu'elle a retourné. La propriété .mock enregistre aussi la valeur de this pour chaque appel, permettant ainsi de l'inspecter :

const myMock1 = jest.fn();
const a = new myMock1();
console.log(myMock1.mock.instances);
// > [ <a> ]

const myMock2 = jest.fn();
const b = {};
const bound = myMock2.bind(b);
bound();
console.log(myMock2.mock.contexts);
// > [ <b> ]

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

// The function was called exactly once
expect(someMockFunction.mock.calls).toHaveLength(1);

// The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');

// The second arg of the first call to the function was 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');

// The return value of the first call to the function was 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value');

// The function was called with a certain `this` context: the `element` object.
expect(someMockFunction.mock.contexts[0]).toBe(element);

// This function was instantiated exactly twice
expect(someMockFunction.mock.instances.length).toBe(2);

// The object returned by the first instantiation of this function
// had a `name` property whose value was set to 'test'
expect(someMockFunction.mock.instances[0].name).toBe('test');

// The first argument of the last call to the function was 'test'
expect(someMockFunction.mock.lastCall[0]).toBe('test');

Valeurs de retour fictives

Les fonctions fictives peuvent aussi 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

Elles sont particulièrement efficaces avec le style de programmation fonctionnelle à continuation. Ce style évite d'avoir à créer des bouchons complexes reproduisant le comportement des composants réels, en privilégiant l'injection directe des valeurs juste avant leur utilisation dans le test.

const filterTestFn = jest.fn();

// Make the mock return `true` for the first call,
// and `false` for the second call
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);

const result = [11, 12].filter(num => filterTestFn(num));

console.log(result);
// > [11]
console.log(filterTestFn.mock.calls[0][0]); // 11
console.log(filterTestFn.mock.calls[1][0]); // 12

La plupart des cas réels impliquent d'obtenir une fonction fictive sur un composant dépendant pour la configurer, mais la technique reste identique. Dans ces situations, résistez à la tentation d'implémenter une logique dans des fonctions qui ne sont pas directement testées.

Simulation de modules

Supposons une classe qui récupère des utilisateurs depuis notre API. Elle utilise axios pour appeler l'API et retourne l'attribut data contenant 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;

Pour tester cette méthode sans réellement interroger l'API (ce qui créerait des tests lents et fragiles), nous pouvons utiliser jest.mock(...) pour simuler automatiquement le module axios.

Après avoir simulé le module, nous pouvons fournir un mockResolvedValue pour .get qui retourne les données à vérifier dans le test. Nous indiquons ainsi que axios.get('/users.json') doit retourner une fausse réponse.

users.test.js
import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);

// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))

return Users.all().then(data => expect(data).toEqual(users));
});

Simulation partielle

Des sous-ensembles d'un module peuvent être simulés tandis que le reste conserve 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');

//Mock the default export and named export 'foo'
return {
__esModule: true,
...originalModule,
default: jest.fn(() => 'mocked baz'),
foo: 'mocked foo',
};
});

test('should do a partial mock', () => {
const defaultExportResult = defaultExport();
expect(defaultExportResult).toBe('mocked baz');
expect(defaultExport).toHaveBeenCalled();

expect(foo).toBe('mocked foo');
expect(bar()).toBe('bar');
});

Implémentations fictives

Certains cas nécessitent d'aller au-delà de la simple définition de valeurs de retour pour remplacer complètement l'implémentation d'une fonction fictive. Ceci peut être réalisé avec jest.fn ou la méthode mockImplementationOnce.

const myMockFn = jest.fn(cb => cb(null, true));

myMockFn((err, val) => console.log(val));
// > true

mockImplementation est utile pour définir l'implémentation par défaut d'une fonction fictive créée à partir d'un autre module :

foo.js
module.exports = function () {
// some implementation;
};
test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');

// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42

Quand vous devez recréer un comportement complexe où des appels successifs produisent des résultats différents, utilisez 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

Quand la fonction simulée épuise les implémentations définies avec mockImplementationOnce, elle exécute l'implémentation par défaut définie via jest.fn (si elle existe) :

const myMockFn = jest
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

Pour les méthodes généralement chaînées (devant toujours retourner this), une API simplifiée sous forme de .mockReturnThis() est disponible sur tous les mocks :

const myObj = {
myMethod: jest.fn().mockReturnThis(),
};

// is the same as

const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};

Noms des mocks

Vous pouvez optionnellement fournir un nom pour vos fonctions fictives, qui sera affiché à la place de 'jest.fn()' dans les sorties d'erreur des tests. Utilisez .mockName() si vous souhaitez identifier rapidement la fonction fictive signalant une erreur dans vos résultats de test.

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

Matchers personnalisés

Enfin, pour faciliter la vérification du mode d'appel des fonctions fictives, nous avons ajouté des fonctions matchers personnalisées :

// The mock function was called at least once
expect(mockFunc).toHaveBeenCalled();

// The mock function was called at least once with the specified args
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);

// The last call to the mock function was called with the specified args
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);

// All calls and the name of the mock is written as a snapshot
expect(mockFunc).toMatchSnapshot();

Ces matchers simplifient les formes courantes d'inspection de la propriété .mock. Vous pouvez toujours effectuer ces vérifications manuellement si vous préférez ou si vous avez besoin d'une approche plus spécifique :

// The mock function was called at least once
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);

// The mock function was called at least once with the specified args
expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);

// The last call to the mock function was called with the specified args
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
arg1,
arg2,
]);

// The first arg of the last call to the mock function was `42`
// (note that there is no sugar helper for this specific assertion)
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);

// A snapshot will check that a mock was invoked the same number of times,
// in the same order, with the same arguments. It will also assert on the name.
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.getMockName()).toBe('a mock name');

Pour une liste complète des matchers, consultez la documentation de référence.