Функції-імітації
Функції-імітації значно спрощують тестування пов’язаного коду, надаючи можливість стирати справжню імплементацію функцї, записувати виклики функції (і параметри, які були їй передані), записувати екземпляри, які повертає функція-конструктор, викликана з допомогою оператора new
і вказувати значення, які має повернути функція під час тестування.
Існує два способи створення функцій-імітацій: створення в коді тестів або написання ручної імітації
для перевизначення залежності модуля.
Використання функцій-імітацій
Давайте уявимо, що ми тестуємо реалізацію функції forEach
, яка викликає коллбек для кожного елементу в наданому масиві.
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}
Щоб протестувати цю функцію, ми можемо використати функцію-імітацію і перевірити її стан, щоб переконатися, що зворотній виклик був викликаний, як і очікувалося.
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
, який містить всіх користувачів:
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 () {
// якась реалізація;
};