Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Манкипатчинг JS: Продвинутое тестирование

В предыдущем уроке мы тестировали гипотетическую функцию getPrivateForksNames(org), применяя инверсию зависимостей. Вспомним содержимое этой функции в её исходном виде:

import Octokit from '@octokit/rest';

const getPrivateForksNames = async (org) => {
  const client = new Octokit();
  const { data } = await client.repos
    .listForOrg({ // Тут сайд-эффекты
      org,
      type: 'private',
    });
  return data.filter(repo => repo.fork).map(repo => repo.name);
};

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

Прототипная модель JS позволяет менять поведение объектов без прямого доступа к ним. Для этого достаточно заменить методы в прототипе. После этого любой объект, имеющий этот прототип, в любой части программы начнет использовать новую реализацию метода.

// Подменяем repos так, чтобы метод listForOrg не делал сетевой запрос
// После выполнения этого кода Octokit меняет свое поведение не только
// в этом модуле, но и вообще по всей программе
Octokit.prototype.repos = {
  listForOrg() {
    console.log('Nothing happens!');
  },
};

// Где-то в другом файле
// Так как объекты передаются по ссылке, то это тот же Octokit
// что и в коде выше
import Octokit from '@octokit/rest';

const client = new Octokit();

// Вызывается подмененный repos
client.repos.listForOrg(/* аргументы не важны, внутри они не используются */);
// => 'Nothing happens!'

В тех случаях, когда объект (например, функция-конструктор) используется напрямую, все еще проще, чем с конструктором. Достаточно поменять свойство самого объекта:

Array.isArray(''); // false

// Этот код может быть вызван в любом месте программы
Array.isArray = () => true;

Array.isArray(''); // true

// То же самое касается любого импортируемого объекта
import Octokit from '@octokit/rest';

// Теперь везде где будет импортироваться Octokit, это будет измененный Octokit
Octokit.boom = () => console.log('Hexlet Magic');

// В любом другом модуле
Octokit.boom(); // => 'Hexlet Magic'

Такой подход, когда глобально подменяются значения свойств, называется манкипатчинг (monkey patching). Он считается плохой практикой при написании обычного кода в JS, но он очень популярен и удобен в тестах.

Самый известный пример в JavaScript-мире – библиотека nock. С её помощью перекрывают реальные сетевые запросы, выполняемые модулем http, который включен в стандартную библиотеку Node.js.

// Пример http-запроса используя модуль http
import http from 'http';

const options = {
  hostname: 'hexlet.io',
  port: 443,
  path: '/my',
  method: 'GET',
};

// request – асинхронный метод
const req = http.request(options, (res) => {
  // Тут обрабатываем http-ответ
});

Nock заменяет внутри модуля http некоторые методы, которые используются разными библиотеками для выполнения HTTP-запросов.

// То как примерно выглядит подмена

import http from 'http';

// Сохраняем старый метод
// Это позволяет вернуть его потом на место
const overriddenRequest = http.request

http.request = (/* тут такие же параметры как и у исходного метода */) => {
  // здесь логика библиотеки nock

  // Возвращаем исходный метод!
  http.request = overriddenRequest;
}

И пример использования:

import nock from 'nock';
import { getPrivateForkNames } from '../src.js'; 

test('getPrivateForkNames', async () => {
  nock(/api\.github\.com/) // это регулярное выражение чтобы не указывать полный адрес
    // get – для GET-запросов, post – для POST-запросов и так далее
    .get(/\/orgs\/hexlet\/repos/)
    .reply(200, [{ fork: true, name: 'one' }, { fork: false, name: 'two' }]);

  const names = await getPrivateForkNames('hexlet');
  expect(names).toEqual(['one']);
});

Цепочка nock(domain).get(uri) задаёт полный адрес страницы, запрос к которой надо перехватить. Nock анализирует все выполняемые запросы и подменяет только тот, который соответствует данным параметрам. Домен и адрес страницы могут указываться как целиком, так и через регулярное выражение, чтобы не писать слишком много.

Метод reply(code, body, headers) описывает ответ, который нужно вернуть по данному запросу. В самом простом случае достаточно указать код возврата. В нашей же ситуации кроме кода нужны данные. Именно на этих данных мы и проверяем работу функции getPrivateForkNames().

Здесь мы рассмотрели только самый базовый вариант использования Nock. У этой библиотеки огромная документация и множество вариантов использования. Полезно периодически её просматривать в поисках более элегантных путей решения задач тестирования.

В чём плюсы и минусы такого способа работы?

Главный плюс в том, что такой способ тестирования практически универсальный. Его можно использовать с любым кодом, без необходимости править сам код. Программа даже не будет догадываться о том, что её тестируют.

Минус же заключается в том, что тестирование "черным ящиком" превращается в тестирование "прозрачным ящиком". Это значит, что тест знает про устройство тестируемого кода и зависит от внутренностей. Такое знание делает тесты хрупкими. Функция может измениться без потери работоспособности, но тесты придётся переписывать, потому что они завязаны на конкретные значения домена, страниц и формата возвращаемых данных.

В большинстве ситуаций это не так критично. Поэтому смело используйте Nock в своих проектах, но не забывайте и про другие способы.


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

  1. Кассеты для Axios

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

Для полного доступа к курсу нужна профессиональная подписка

Профессиональная подписка откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
900
упражнения
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 120 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов веб-приложений
22 сентября 8 месяцев
Иконка программы Node.js-разработчик
Профессия
Разработка бэкенд-компонентов веб-приложений
22 сентября 8 месяцев

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»