В некоторых ситуациях инверсия зависимостей подходит идеально, в других из-за нее код становится значительно сложнее и иногда запутаннее, особенно если зависимости требуются где-то глубоко в стеке вызовов (так как придется пробрасывать зависимость через все промежуточные функции). Но есть способ, который позволяет добраться до нужных вызовов и изменить их даже без инверсии зависимостей.
Ниже пример класса, в котором метод делает запрос по сети:
import axios from 'axios';
class Api {
// ...
async makeRequest(url) {
const { data } = await axios.get(url);
this.data = data;
}
}
Прототипная модель JS позволяет менять поведение объектов без прямого доступа к ним. Для этого достаточно заменить методы в прототипе. После этого любой объект, имеющий этот прототип, в любой части программы начнет использовать новую реализацию метода.
Используем этот подход, чтобы не дать приложению делать реальный сетевой запрос. Для этого подменим метод:
// Подменяем метод makeRequest так, чтобы он не делал сетевой запрос
// После выполнения этого кода Api меняет свое поведение не только
// в этом модуле, но и вообще по всей программе
Api.prototype.makeRequest = function() {
console.log('no request');
this.data = 'test data';
};
// Где-то в другом файле
// Так как объекты передаются по ссылке, то это тот же Api
// что и в коде выше
import Api from './api.js';
const client = new Api();
// Вызывается подмененный makeRequest
client.makeRequest(/* аргументы не важны, внутри они не используются */);
// => 'no request'
В тех случаях, когда объект (например, функция-конструктор) используется напрямую, все еще проще, чем с конструктором. Достаточно поменять свойство самого объекта:
Array.isArray(''); // false
// Этот код может быть вызван в любом месте программы
Array.isArray = () => true;
Array.isArray(''); // true
// То же самое касается любого импортируемого объекта
import Api from './api.js';
// Теперь везде, где будет импортироваться Api, это будет измененный Api
Api.boom = () => console.log('Hexlet Magic');
// В любом другом модуле
Api.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(url)
задает полный адрес страницы, запрос к которой надо перехватить. Nock анализирует все выполняемые запросы и подменяет только тот, который соответствует данным параметрам. Домен и адрес страницы могут указываться как целиком, так и через регулярное выражение, чтобы не писать слишком много.
Метод reply(code, body, headers)
описывает ответ, который нужно вернуть по данному запросу. В самом простом случае достаточно указать код возврата. В нашей же ситуации кроме кода нужны данные. Именно на этих данных мы и проверяем работу функции getPrivateForkNames()
.
Здесь мы рассмотрели только самый базовый вариант использования Nock. У этой библиотеки огромная документация и множество вариантов использования. Полезно периодически ее просматривать в поисках более элегантных путей решения задач тестирования.
В чем плюсы и минусы такого способа работы?
Главный плюс в том, что такой способ тестирования практически универсальный. Его можно использовать с любым кодом, без необходимости править сам код. Программа даже не будет догадываться, что ее тестируют.
Минус заключается в том, что тестирование черным ящиком превращается в тестирование прозрачным ящиком. Это значит, что тест знает про устройство тестируемого кода и зависит от внутренностей. Такое знание делает тесты хрупкими. Функция может измениться без потери работоспособности, но тесты придется переписывать, потому что они завязаны на конкретные значения домена, страниц и формата возвращаемых данных.
В большинстве ситуаций это не так критично. Поэтому смело используйте Nock в своих проектах, но не забывайте и про другие способы.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.