Fonctions simulées
Mock functions allow you to test the links between code by erasing the actual implementation of a function, capturing calls to the function (and the parameters passed in those calls), capturing instances of constructor functions when instantiated with new, and allowing test-time configuration of return values.
There are two ways to mock functions: Either by creating a mock function to use in test code, or writing a manual mock to override a module dependency.
Utilisation d'une fonction simulée
Let's imagine we're testing an implementation of a function forEach, which invokes a callback for each item in a supplied array.
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}
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 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);
});
.mock property
All mock functions have this special .mock property, which is where data about how the function has been called and what the function returned is kept. The .mock property also tracks the value of this for each call, so it is possible to inspect this as well:
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 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 :
// 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 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();
// 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 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. The class uses axios to call the API then returns the data attribute which contains all the users:
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
Now, in order to test this method without actually hitting the API (and thus creating slow and fragile tests), we can use the jest.mock(...) function to automatically mock the axios module.
Once we mock the module we can provide a mockResolvedValue for .get that returns the data we want our test to assert against. In effect, we are saying that we want axios.get('/users.json') to return a fake response.
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
Les sous-ensembles d'un module peuvent être simulés et le reste du module peut conserver son implémentation réelle :
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');
});