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

Насмешка над одной функцией

Рассмотрим следующую функцию proxy().

function proxy(data, callback) {
  return callback(data);
}

module.exports = proxy;

Тестирование исполнения

Чтобы подтвердить, что функция callback() действительно вызывается при выполнении функции proxy(), мы можем:

  1. Создайте макет функции callback(), используя функцию jest.fn().
  2. Выполните функцию proxy(), используя макет mockFn.
  3. Проверьте, был ли вызван макет, сколько раз он был вызван и с какими параметрами, используя следующие самоописательные средства сопоставления.
const proxy = require('./proxy');

test('it should invoke the callback function', () => {
  // (1)
  const mockFn = jest.fn();

  // (2)
  proxy('Hello World', mockFn);

  // (3)
  expect(mockFn).toHaveBeenCalled();
  expect(mockFn).toHaveBeenCalledTimes(1);
  expect(mockFn).toHaveBeenCalledWith('Hello World');
});

Переопределение реализации

Чтобы переопределить реализацию функции и, следовательно, ее поведение, мы можем определить ее новую реализацию внутри самой функции jest.fn() и проверить ее результат с помощью сопоставителя toHaveReturnedWith().

const proxy = require('./proxy');

test('it should return the data length', () => {
  const mockFn = jest.fn(data => data && data.length || 0);

  proxy('Hello World', mockFn);

  expect(mockFn).toHaveReturnedWith(12);
});

Переопределение возвращаемого значения

Чтобы переопределить возвращаемое значение функции вместо ее реализации, мы можем использовать функцию mockReturnValue(), которая будет возвращать одно и то же значение для каждого вызова.

const proxy = require('./proxy');

test('it should return the data length', () => {
  const mockFn = jest.fn().mockReturnValue(20);
  
  proxy('Hello world', mockFn);

  expect(mockFn).toHaveReturnedWith(20);

  proxy('Bonjour le monde', mockFn);

  expect(mockFn).toHaveReturnedWith(20);
});

Или мы можем использовать функцию mockReturnValueOnce(), которая будет возвращать определенное значение только для следующего вызова.

test('it should return different data lengths', () => {
  const mockFn = jest.fn()
    .mockReturnValueOnce(3)
    .mockReturnValueOnce(5);

  proxy('Hello', mockFn);

  expect(mockFn).toHaveReturnedWith(3);

  proxy('Hello', mockFn);

  expect(mockFn).toHaveReturnedWith(5);
});

Насмешка над классом

Давайте рассмотрим следующий класс User, конструктор которого принимает в качестве аргумента строку, представляющую имя, и реализует функцию hello(), которая возвращает строку, содержащую свойство name.

class User {
  constructor(name) {
    this.name = name;
  }

  hello() {
    return "Hello " + this.name;
  }
}

module.exports = User;

Издевательство над целым классом

Поскольку классы ES6 по сути являются функциями-конструкторами с некоторым синтаксическим сахаром, любой макет для класса ES6 должен быть функцией или фактическим классом ES6 (что, опять же, является другой функцией), и, следовательно, может быть смоделирован с помощью функции jest.fn().

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

const MyClass = require('./myclass');

jest.mock('./myclass', () => {
  return jest.fn(() => {
    return {
      // ...
    };
  });
});

Как вы можете видеть в этом примере, если мы имитируем класс User следующим образом и создаем экземпляр нового объекта, используя строку Jack, значение, содержащееся в свойстве name, на самом деле будет тем, которое определено в макете, а не тем, которое передано в конструктор.

const User = require('./user');

jest.mock('./user', () => {
  return jest.fn(() => {
    return {
      name: 'John',
      hello: jest.fn()
    };
  });
});

test('it should mock the User constructor', () => {
  const user = new User('Jack');

  expect(user.name).toBe('John');
});

В качестве альтернативы мы также можем имитировать класс для каждого теста — аналогично функциям — используя метод mockImplementationOnce() следующим образом.

const User = require('./user');

jest.mock('./user');

test('it should mock the User constructor', () => {
  User.mockImplementationOnce(() => {
    return {
      name: 'John',
      hello: jest.fn()
    };
  });

  const user = new User('Jack');

  expect(user.name).toBe('John');
});

Шпионить за методом класса

Чтобы протестировать поведение класса без изменения его фактической реализации, мы можем использовать функцию jest.spyOn(), которая принимает в качестве аргумента объект и метод для отслеживания вызовов и возвращает фиктивную функцию.

const User = require('./user');

test('it should return the string "Hello John"', () => {
  const user = new User('John');

  const mock = jest.spyOn(User.prototype, 'hello');

  user.hello();

  expect(mock).toHaveReturnedWith("Hello John");
});

Насмешка над методом класса

Чтобы имитировать конкретный метод класса без изменения каких-либо других свойств, мы можем использовать функцию jest.spyOn(), которую мы только что видели, и связать ее с методом .mockImplementationOnce().

const User = require('./user');

test('it should return the string "Hello John"', () => {
  const user = new User('John');

  const mock = jest
    .spyOn(User.prototype, 'hello')
    .mockImplementationOnce(() => {
      return "Gutten Tag John";
    });

  user.hello();

  expect(mock).toHaveReturnedWith("Gutten Tag John");
});

Издевательство над модулем

Создание макета всего модуля на самом деле похоже на создание макета класса, поскольку это можно сделать непосредственно в функции обратного вызова функции jest.mock().

jest.mock('module', () => {
  return jest.fn(() => {
    return {
      // ...
    };
  });
});

Давайте рассмотрим следующий модуль, который создает экземпляр нового обработчика базы данных, используя класс Sequelize, и выполняет попытку подключения к базе данных, используя метод authenticate() объекта обработчика базы данных.

const Sequelize = require('sequelize');

async function connect(env) {
  const db = new Sequelize(env.name, env.user, env.password, {
    host: env.host,
    port: env.port,
    dialect: env.dialect,
    logging: env.logging
  });
  
  await db.authenticate();

  return db;
};

module.exports = connect;

Чтобы утверждать, что функция connect вызывает класс Sequelize и метод authenticate(), фактически не выполняя попытку соединения, мы можем имитировать модуль Sequelize следующим образом:

const Sequelize = require('sequelize');
const connect = require('./connect');

jest.mock('sequelize', () => {
  return jest.fn(() => {
    return {
      authenticate: jest.fn()
    };
  });
});

afterEach(() => jest.clearAllMocks());

const env = {
  name: 'database',
  user: 'john',
  password: 'hello',
  host: '127.0.0.1',
  port: 3306,
  dialect: 'mysql',
  logging: true
};

test('it should invoke the Sequelize constructor', async () => {
  await connect(env);

  expect(Sequelize).toHaveBeenCalledWith(env.name, env.user, env.password, {
    host: env.host,
    port: env.port,
    dialect: env.dialect,
    logging: env.logging
  });
});

test('it should invoke the authenticate handler', async () => {
  const db = await connect(env);

  expect(db.authenticate).toHaveBeenCalledTimes(1);
});

Хотите узнать больше?

Узнайте, как создавать реальные API в Node.js от первой строки кода до последней строки документации, из книги Build Layered Microservices, доступной по адресу learnbackend.dev.

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .

Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.