Вторая же — маги. Кто-то, считает их шарлатанами, и уж никто точно не понимает как работает их код. Но он работает. На строгую типизацию у них табу, а про(от) DI у них есть простая отговорка:
«Зачем мне уродовать свой код, смешивая ужа с ежом, если это нужно исключительно для тестов?».
И ведь на самом деле — добавлять в проект DI исключительно чтобы мокать зависимости в тестах — идея не самая умная. Особенно если DI и на самом деле редкий зверь за пределами экосистемы Angular.
Есть только одно но — если технари от своей профдеформации не страдают, то маги… ну как сказать…
В общем пару месяцев назад один добрый человек создал мне в proxyquire-webpack-alias issue. Суть была проста — «не работает». Мне потребовался день чтобы изменить ЧТО не работает, на ГДЕ.
![](https://habrastorage.org/webt/59/d9/83/59d983d337104040855863.jpeg)
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.
![](https://habrastorage.org/webt/59/d9/83/59d983d2c366f750359146.jpeg)
Хотя. Кому он нужен под webpack то? Вот честно — поднимите руки, а то у меня знакомых которые сидят на Karma/Headless/Webpack/Angular, и которые могут проверить это все в деле – как-то не завелось.
PS: Он и под ноду то особенно не востребован. Старый добрый proxyquire, несмотря на все свои ограничения, справляется с 99% задач. О значимой разнице знаю только я…
Комментарии (9)
AlexPu
09.10.2017 09:12>> DI и на самом деле редкий зверь за пределами экосистемы Angular
Дальше читать не сталkashey Автор
09.10.2017 10:22+1Но «нормальный» DI(уровня SpringBoot/ng-di) на самом деле редкий зверь в мире JavaScript. Есть взять пяток самых популярный библиотек, убрать то что для ng, и посмотреть на статистику скачек — будет не очень. Плюс многие путают DI с более широким IoT.
DI хорош, когда в одну и туже розетку можно засунуть разные вилки. Если пара вилка/розетка всегда одна, и DI — исключительно для тестов — может его и не надо?
И, самое главное, многие вещи просто принято использовать «как есть». Именно поэтому 90% примеров моканья чего либо, мокают: fs, сеть, session storage, селекторы в редаксе и другой environment.AlexPu
09.10.2017 13:59>>может его и не надо?
Что и когда надо или НЕ надо это вопрос за который я, как и большинство моих коллег получаем заработную плату. Поэтому, безапелляционные утверждения, основаные в основном на недостаточной информированности вызывают у меня легкое чуство раздражения (с пониманием того факта, что когда опять все станет совсем плохо все равно позовут меня или какого нибудь из моих коллег, которые не склонны к радикальным взглядам на что бы то ни было)kashey Автор
09.10.2017 14:10+2У вас метод индукции сбоит. В данный момент безаппеляционные утверждения исходят только с вашей стороны.
Вроде как приверженность только одного взгляда — и есть проявление радикального.
У меня каждый день есть возможности видеть плюсы и минусы разных подходов на разных языках… и в большинстве случаев использование «классического» DI — это или Java, или объектно ориентированный говнокод 5-то летней давности.
justboris
По умолчанию Karma-webpack собирает независимый бандл для каждого теста. Поэтому проблема очищения моков после каждого теста не стоит.
Сейчас собрал примитивный пример с
inject-loader
, работает как надо.vasIvas
А Вам не кажется, что это похоже на открывание консервов гидравлическими ножницами?
Ведь это разные слои, webpack для внешних зависимостей, DI для зависимостей приложения.
Зачем смешивать? [ прошу прощения, ни туда написал ]
justboris
На эту тему автор уже отвечал в комментариях к прошлой статье: https://habrahabr.ru/post/329740/#comment_10258456
если у вам повезло, и у вас есть нормальный DI, то у вас все хорошо. А если не повезло — то proxyquire и компания — неплохой способ выкрутиться без переписывания кода целиком.
kashey Автор
У меня и самого есть проект на inject-loader, и я им в принципе доволен. Но иногда по пути попадаются грабли, на которые редко, да наступаю.
Давайте придумаем немного синтетический пример:
После чего мы просто мокаем module1 и expect(function1).to.be.called();
Проходит время, и Вася добавляет еще одну строчку:
Но он не меняет тесты, а тесты как работали, так и работают.
Бывает и обратная ситуация — вы убираете строчку, а тесты как работали, так и работают.
Беда в том, что тесты должны не только «тестировать» поведение функции, те проверять результат — они должны служить вторым контуром проверки, обеспечивать еще один «момент подумать».
И, по хорошему, должны падать при _любом_ изменении логики програмы. Просто потому что они должны его детектить и подносить вам на блюдечке.
Да — совсем не круто править 10 тестов после того как изменил одну запятую. Но еще более не круто – не догадываться, что вы изменили чуть больше логики, чем думаете, этой одной запятой.
В свое время я пытался пропихнуть API для этого в proxyquire, но не срослось.
В rewiremock для мока можно определить то как он может быть вызван — toBeUsed, directChildOnly, calledFromMock, плюс режим isolation который будет ругаться на каждый неожиданный import.
Раз уж units tests are production code — относитесь к ним соотвествующе. Хотя конечно можно добавить тесты для тестов, но нельзя добавить тесты для моков, те заставить моки соотвествовать интерфейсам того, кого они собой замещают – тут нужен DI. Но он тоже не сахар.