Перейти до основного змісту
Версія: Next

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

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

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

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

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

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

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

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

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

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

// Функція-імітація викликається двічі
expect(mockCallback.mock.calls).toHaveLength(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 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> ]

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

// Функція викликається рівно один раз
expect(someMockFunction.mock.calls).toHaveLength(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');

// Функція викликається з певним контекстом `this`: об'єкт `element`.
expect(someMockFunction.mock.contexts[0]).toBe(element);

// Екземпляри цієї функції створюються рівно двічі
expect(someMockFunction.mock.instances.length).toBe(2);

// Перший екземпляр функції повертає об'єкт
// з властивістю `name`, значення якої 'test'
expect(someMockFunction.mock.instances[0].name).toBe('test');

// Перший аргумент останнього виклику функції був 'test'
expect(someMockFunction.mock.lastCall[0]).toBe('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();

// Налаштовуємо імітацію на повернення `true` після першого
// і `false` після другого виклику
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

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

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

Припустимо, що в нас є клас, який отримує користувачів з нашого 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);

// або ви можете використовувати наступну конструкцію, в залежності від ваших потреб:
// 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 () {
// якась реалізація;
};
test.js
jest.mock('../foo'); // це відбувається автоматично, коли увімкнений автоматичний мокінг
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

За потреби, ви можете вказати імена для ваших функцій-імітацій, які будуть показуватися замість 'jest.fn()' у виводі помилок тестів. Use .mockName() 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');

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