Перейти до основного змісту
Version: 25.x

Функції-імітації

Функції-імітації значно спрощують тестування пов’язаного коду, надаючи можливість стирати справжню імплементацію функцї, записувати виклики функції (і параметри, які були їй передані), записувати екземпляри, які повертає функція-конструктор, викликана з допомогою оператора new і вказувати значення, які має повернути функція під час тестування.

Існує два способи створення функцій-імітацій: створення в коді тестів або написання ручної імітації для перевизначення залежності модуля.

Використання функцій-імітацій#

Давайте уявимо, що ми тестуємо реалізацію функції forEach, яка викликає коллбек для кожного елементу в наданому масиві.

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

Щоб протестувати цю функцію, ми можемо використати функцію-імітацію і перевірити її стан, щоб переконатися, що зворотній виклик бул викликаний, як і очікувалося.

const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
// Функція-імітація викликана двічі
expect(mockCallback.mock.calls.length).toBe(2);
// Перший аргумент першого виклтку функції був 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// Перший аргумент другого виклику функції був 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// Значення, поверненене в результаті першого виклику було 42
expect(mockCallback.mock.results[0].value).toBe(42);

Властивість .mock#

Всі функції-імітації маюь спеціальну властивість .mock, де зберігається інформація про те, як функція була викликана і які значення вона повертала. Властивість .mock також відслідковує значення this для кожного виклику, що дозволяє вивчати їх пізніше:

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

Наступні властивості функцій-імітацій дуже корисні в тестах для перевірки того, як ці функції були викликані, які екземпляри були створені або які значення вони повернули:

// Функція була викликана рівно один раз
expect(someMockFunction.mock.calls.length).toBe(1);
// Перший агрумент першого виклику був 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
// Другий агрумент другого виклику був 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
// Значення, повернене після першого виклику функції було 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value');
// Було створено рівно два екземпляри
expect(someMockFunction.mock.instances.length).toBe(2);
// Перший екземпляр об’єкта, створений функцією, має
// властивість `name` зі значенням `test`
expect(someMockFunction.mock.instances[0].name).toEqual('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

Функції-імітації також дуже ефективні для тестування коду, який використовує функціональний стиль. Код, написаний в такому стилі, дозволяє уникати складної підготовки для відтворення поведінки реального компоненту, в якому він використовується, на користь передачі значень прямо в тест безпосередньо перед тим, як вони будуть використані.

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);
// > [ [11], [12] ]

Більшість прикладів з реального життя передбачають створення функцій-імітацій в компонентах, від яких залежить ваш код, але техніка використовується та ж сама. В такому випадку намагайтеся уникати спокуси імплементувати логіку всередині будь-якої функції, яка безпосередньо не тестується.

Імітація модулів#

Припустимо, що в нас є клас, який отримує користувачів з нашого API. Клас використовує axios для виклику API і повертає атрибут data, який містить всіх користувачів:

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

Тепер, щоб протестувати цей метод без справжнього API виклику (тобто не створюючи повільні і крихкі тести), ми можемо використати jest.mock(...) для створення імітації всього модуля axios.

Після того, як ми створимо імітацію модуля, ми можемо вказати mockResolvedValue для методу .get, який повертатиме дані, з якими працюватиме наш тест. Тобто фактично ми говоримо, що хочемо, щоб axios.get('/users.json') повернув фільшиву відповідь.

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

Часткова імітація#

Існує можливість створити імітацію тільки для частини модуля, коли решта методів зберігатиме свою оригінальну реалізацію:

// 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');
// Створюємо імітацію еспорту за-замовчуванням і іменованого експорту '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');
});

Реалізація імітації#

Still, there are cases where it's useful to go beyond the ability to specify return values and full-on replace the implementation of a mock function. This can be done with jest.fn or the mockImplementationOnce method on mock functions.

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

The mockImplementation method is useful when you need to define the default implementation of a mock function that is created from another 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

When you need to recreate a complex behavior of a mock function such that multiple function calls produce different results, use the mockImplementationOnce method:

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

When the mocked function runs out of implementations defined with mockImplementationOnce, it will execute the default implementation set with jest.fn (if it is defined):

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

For cases where we have methods that are typically chained (and thus always need to return this), we have a sugary API to simplify this in the form of a .mockReturnThis() function that also sits on all mocks:

const myObj = {
myMethod: jest.fn().mockReturnThis(),
};
// is the same as
const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};

Mock Names#

You can optionally provide a name for your mock functions, which will be displayed instead of "jest.fn()" in the test error output. Use this if you want to be able to quickly identify the mock function reporting an error in your test output.

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

Користувацькі матчери#

Finally, in order to make it less demanding to assert how mock functions have been called, we've added some custom matcher functions for you:

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

These matchers are sugar for common forms of inspecting the .mock property. You can always do this manually yourself if that's more to your taste or if you need to do something more specific:

// 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 of an 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');

Для повного списку матчерів зверніться до довідкової документації.