В мире 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. Добавить «хоть какие-то»
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% задач. О значимой разнице знаю только я…
Автор: Корзунов Антон