Функції-імітації
Функції-імітації значно спрощують тестування пов’язаного коду, надаючи можливість стирати справжню імплементацію функцї, записувати виклики функції (і параметри, які були їй передані), записувати екземпляри, які повертає функція-конструктор, викликана з допомогою оператора new
і вказувати значення, які має повернути функція під час тестування.
Існує два способи створення функцій-імітацій: створення в коді тестів або написання ручної імітації
для перевизначення залежності модуля.
Використання функцій-імітацій
Давайте уявимо, що ми тестуємо реалізацію функції forEach
, яка викликає коллбек для кожного елементу в наданому масиві.
export function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
Щоб протестувати цю функцію, ми можемо використати функцію-імітацію і перевірити її стан, щоб переконатися, що зворотній виклик був викликаний, як і очікувалося.
import {forEach} from './forEach';
const mockCallback = jest.fn(x => 42 + x);
test('forEach 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
Всі функції-імітації мають спеціальну властивість .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> ]
Наступні властивості функцій-імітацій дуже корисні в тестах для перевірки того, як ці функції були викликані, які екземпляри були створені або які значення вони повернули:
// 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');
// 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');
Імітація повернених значень
Функції-імітації також можуть бути використані, щоб передавати тестові значення у ваш код під час тесту:
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
, який містить всіх користувачів:
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')
повернув фільшиву відповідь.
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));
});
Часткова імітація
Існує можливість створити імітацію тільки для частини модуля, коли решта методів зберігатиме свою оригінальну реалізацію:
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:
module.exports = function () {
// якась реалізація;
};
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()'
у виводі помилок тестів. Використовуйте .mockName()
, якщо вам потрібно швидко визначити функцію-імітацію, що звітує про помилку у виводі тесту.
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');
Для повного списку матчерів зверніться до довідкової документації.