javascript

Webpack и моканье зависимостей

  • понедельник, 9 октября 2017 г. в 03:12:40
https://habrahabr.ru/post/339590/
  • JavaScript


В мире JavaScript существуют две фракции. Первая из них — технари, которые все проблемы стараются решать «технично». Вообще технари ребята суровые, я бы даже сказал строгие, и потому любят такую же суровую и строгую типизацию, и везде суют TypeScript, Dependency Injection и другой IoC.

Вторая же — маги. Кто-то, считает их шарлатанами, и уж никто точно не понимает как работает их код. Но он работает. На строгую типизацию у них табу, а про(от) DI у них есть простая отговорка:

«Зачем мне уродовать свой код, смешивая ужа с ежом, если это нужно исключительно для тестов?».

И ведь на самом деле — добавлять в проект DI исключительно чтобы мокать зависимости в тестах — идея не самая умная. Особенно если DI и на самом деле редкий зверь за пределами экосистемы Angular.

Есть только одно но — если технари от своей профдеформации не страдают, то маги… ну как сказать…

В общем пару месяцев назад один добрый человек создал мне в proxyquire-webpack-alias issue. Суть была проста — «не работает». Мне потребовался день чтобы изменить ЧТО не работает, на ГДЕ.



PS: Зачем нужно заменять (мокать) зависимости в тестах? Чтобы тесты были более «юнит», более изолированные, и не дергали реальные команды (ендпоинты), которые могут быть и очень медленными, и очень одноразовыми. В общем не надо трогать их в тестах.

Суть проблемы очень проста – для моканья зависимостей в nodejs придумано ОЧЕНЬ много библиотек: proxyquire, rewire, mockery и так далее. Все они паразитируют на внутренем представлении nodejs о модулях и их шупальца пробираются куда-то в начинку require.

Если же ваши тесты запускаются не в nodejs, а в браузере – то все меняется. Банально — нет никакого nodejs environment, только тот суррогат, что предоставил использованный бандлер. Но, так как бандеров вообще неограниченное колличество — будем раcсматривать только один — webpack.

Тем более, что некоторые бандлеры, типа browseryfy или (особенно) rollup «модульной» системы не имеют вообще. И не надо.

Webpack


Исторически сложилось, что есть только один подход к моканью зависимостей в webpack — использовать inject-loader или rewire, который тоже «loader».

Лоадеры просто «меняют» запрашиваемый файл на уровне исходных кодов. К принципе особых притензий к таким загрузчикам нету — вы просто говорите

const stuff = require('inject-loader!stuff')({
  'fs': mockFS,
  'someOtherDep': mock
});

И зависимости будут замоканы. Ну просто это немного каменный век, и использовать все это дело не так чтобы всегда просто. «Старшие братья» из nodejs (особенно mockery) умеют сильно сильно больше.

Со rewire сложнее. Я бы лично ломал бы пальцы тем кто его использует — «мокать» используя rewire это тоже самое, что «мокать» используя sinon.

С другой стороны — rewire-webpack это единственный(!) правильный на уровне исходных кодов плагин(не лоадер) для webpack. Просто потому что автор rewire зафейлил этот плагин написать, и его писал автор webpack. Хотя этот плагин в итоге просто добавляет loader. Я вот тоже запутался.
Большой плюс rewire, несмотря на его кракозяблость,- одинаковый интерфейс для webpack и node окружения. Он был один такой хороший.

Rewiremock


Несколько месяцев назад я написал чуть более «правильный», чем остальные, инструмент для моканья зависимостей — Rewiremock (github, статья на хабре). И мне было прямо «спортивно интересно» завести rewiremock не только под nodejs(с который вообще проблем нет), но и под webpack.

И так чтобы API не изменился, и все тесты работали. Сейчас не работает один тест, потому что не должен. А все остальные — зеленые.

1. Как оно работает?


Вся работа свелась буквально к трем пунтам:

1. Добавить «хоть какие-то» мозги модульной системе webpack. А именно требуется добавить два плагина, и оба с некой вероятностью уже есть — NamedModulesPlugin(который вернет файлам имена) и HotModuleReplacementPlugin(который предоставит некий сурогат модульной системы), плюс подключить плагин от rewiremock(который заменит require на свой вариант).
2. Дописать недостающий тулинг — очистка кеша, работа с чуть чуть другим «module».
3. Собственно нарисовать плагин, который как-то внедрит возможность перегрузки require.

Проблема возникла только с третьим пунктом — никакого хепла, доки или примера о том как можно сделать желаемое я найти не смог. Документация webpack часто отправляет курить сорсы, что тоже было проделано, только ясности не внесло.

Дальнейшее ознакомление с исходниками rewire-webpack, который, как я уже говорил, единственно правильный, решение подсказало. Точнее стало понятно, что просто все очень плохо.

Webpack большей частью основан на Tappable — маленькой библиотечке, которая вызывает хуки в некой последовательности. Это вроде как lifecycle… но что и когда она вызывает, какие аргументы передать, и (главное!) что с ними можно сделать — информации ноль. В общем webpack писали маги, а не технари.

Сейчас все думаю — оставиль свою реализацию плагина как есть, или заменить на более «правильное» из rewire. Оно просто раз в 5 длинее и смысла я в этом покуда не вижу.

2. Что в итоге?


В итоге — оно просто работает. Где-то внутри зашито многовато магии по нормализации имен файлов, чтобы все прозрачно работало в обоих экосистемах, но наружу точит достаточно простой и удобный API. Точнее целый букет, который не изменился.

Одна из «проблем» rewiremock — универсальность API.

Он может работать как mockery (базовый синтаксис как у mockery, включая режим изоляции):

rewiremock('fs')
    .with({
        readFile: yourFunction
    });
rewiremock.enable();

Может как proxyquire (хеплеры proxy и module):

rewiremock.proxy('somemodule', {
   'dep1': { name: 'override' },
   'dep2': { name: 'override' }
 }));

Умеет разные чтуки из Jest (например динамическое создание мока):

rewiremock('fs')
    .by(({requireActual}) => requireActual('fs'));

И есть кой какие свои приемы (расширенный синтаксис proxy):

const mock = await rewiremock.module(() => import('somemodule'), r => ({
   'dep1': r.with({ name: 'override' }).calledFromMock(),
}));

В общем, как я уже писал выше, rewiremock — инструмент чуть более лучший, чем все остальные. И первый, из «нормальных», который одинаково умеет работать как как под nodejs, так и под webpack.



Хотя. Кому он нужен под webpack то? Вот честно — поднимите руки, а то у меня знакомых которые сидят на Karma/Headless/Webpack/Angular, и которые могут проверить это все в деле – как-то не завелось.

PS: Он и под ноду то особенно не востребован. Старый добрый proxyquire, несмотря на все свои ограничения, справляется с 99% задач. О значимой разнице знаю только я…