Spec-Zone.ru › Jest

Мок-функции

Мок-функции позволяют проверять связи между частями кода, удаляя фактическую реализацию функции, отслеживая вызовы функции (и переданные при этих вызовах параметры), отслеживая экземпляры функций-конструкторов, созданные с помощью new, а также настраивая возвращаемые значения во время тестирования.

Есть два способа создавать моки функций: создать мок-функцию для использования в тестовом коде или написать manual mock, чтобы переопределить зависимость модуля.

Использование мок-функции

Представим, что мы тестируем реализацию функции forEach, которая вызывает обратный вызов для каждого элемента переданного массива.

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

Чтобы протестировать эту функцию, мы можем использовать мок-функцию и проверить её состояние, чтобы убедиться, что обратный вызов вызывается ожидаемым образом.

import {forEach} from './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);
});
forEach.test.js

Свойство .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> ]

Эти свойства мок-функций очень полезны в тестах для проверки того, как вызываются или создаются экземпляры этих функций, а также что они возвращают:

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

Возвращаемые значения мок-функций

Мок-функции также можно использовать для передачи тестовых значений в код во время тестирования:

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[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;
users.js

Чтобы протестировать этот метод, не обращаясь к 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);

  // 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));
});
users.test.js

Частичное мокирование

Можно создать моки для отдельных частей модуля, оставив фактическую реализацию остальных частей без изменений:

export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
foo-bar-baz.js
//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');
});

Реализации моков

Тем не менее иногда полезно не ограничиваться указанием возвращаемых значений и полностью заменять реализацию мок-функции. Это можно сделать с помощью jest.fn или метода mockImplementationOnce мок-функций.

const myMockFn = jest.fn(cb => cb(null, true));

myMockFn((err, val) => console.log(val));
// > true

Метод mockImplementation полезен, когда нужно определить реализацию по умолчанию для мок-функции, созданной из другого модуля:

module.exports = function () {
  // some implementation;
};
foo.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');

// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42
test.js

Если нужно воссоздать сложное поведение мок-функции, при котором разные вызовы функции дают разные результаты, используйте метод mockImplementationOnce:

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

Когда у мокируемой функции заканчиваются реализации, заданные с помощью mockImplementationOnce, она выполнит реализацию по умолчанию, заданную с помощью jest.fn (если она определена):

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

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

Для методов, вызовы которых обычно объединяются в цепочки (и поэтому они всегда должны возвращать this), предусмотрен упрощённый API в виде функции .mockReturnThis(), доступной также для всех моков:

const myObj = {
  myMethod: jest.fn().mockReturnThis(),
};

// is the same as

const otherObj = {
  myMethod: jest.fn(function () {
    return this;
  }),
};

Имена моков

При желании можно задать имена мок-функциям. Они будут отображаться в сообщениях об ошибках теста вместо 'jest.fn()'. Используйте .mockName(), чтобы быстро определить, какая мок-функция вызвала ошибку в выводе теста.

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

Пользовательские сопоставители

Наконец, чтобы упростить проверку вызовов мок-функций, мы добавили несколько пользовательских функций-сопоставителей:

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

Эти сопоставители представляют собой сокращённую запись для распространённых способов проверки свойства .mock. Если вам так удобнее или требуется что-то более специфичное, всегда можно выполнить эту проверку вручную:

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

Полный список сопоставителей см. в справочной документации.

© 2022 Facebook, Inc.
Licensed under the MIT License.
https://jestjs.io/docs/mock-functions

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API