Источник: Nuances of Programming
Каждый разработчик и команда разработчиков должны принимать решения при создании нового проекта. Сегодня обсудим проекты Node.js. Если говорить о разработке на JavaScript, то одним из лучших решений, которое вы можете принять, является выбор TypeScript. Этот язык дает дополнительные инструменты для написания более понятного и поддерживаемого кода.
Еще одно удачное решение — добавить в проект тестовый фреймворк или библиотеку. Сейчас наиболее часто используемым фреймворком является Jest. Его выбор вполне оправдан, поскольку он снабжен мощной встроенной библиотекой для проверки утверждений.
И последнее, но не менее важное — это конвейер. Он поможет избежать ошибок и обеспечит слаженную работу перед каждым слиянием и релизом. Для этой задачи воспользуемся Jenkins, хотя тут сгодится любой другой вариант.
Итак, вы уже знакомы с тремя главными “героями” этой статьи: TypeScript, Jest.js и Jenkins. Приступим.
Проблема
Все началось с того, что меня попросили обновить зависимости проекта, поскольку были обнаружены некоторые уязвимости и прошло много времени с момента последнего обновления. Я обновил следующие зависимости (упомяну наиболее важные):
typescript from 3.9.7 to 4.7.4
jest from 26.4.2 to 28.1.1
ts-jest from 26.3.0 to 28.0.5
Затем, когда я ввел изменения и Jenkins запустил тесты, я столкнулся с двумя разными проблемами на двух этапах.
- На этапе тестирования происходило застревание на случайном тестовом примере при каждом выполнении конвейера, что мешало завершению тестирования.
- После реализации обходного решения для первой проблемы и слияния изменений в другую функциональную ветку с дополнительным кодом и тестами, в лог выводилась следующая ошибка: JavaScript heap out of memory error (Ошибка нехватки памяти в куче JavaScript).
Кроме того, важно отметить, что до обновления зависимостей этап тестирования занимал 1,59 минуты для модульных тестов и 3,26 минуты для e2e-тестов. Запомните эти показатели — мы вернемся к ним чуть позже.
Решение проблемы застревания во время выполнения тестовых примеров
Как видно из ответов на этот вопрос на Stack Overflow, наиболее распространенный подход к исправлению данного сбоя связан с количеством потоков, используемых Jest для запуска тестов. Вы можете изменить поведение по умолчанию, используя две различные опции.
- — runInBand последовательно запускает все тесты и использует основной процесс вместо создания дополнительных потоков.
- — maxWorkers позволяет настраивать количество потоков, которые можно задействовать для выполнения тестов. По умолчанию это значение равно количеству ядер, доступных на вашем компьютере за вычетом одного для основного потока.
Поэтому я добавил опцию -runInBand в скрипт тестирования package.json, чтобы не позволить Jest запускать более одного теста одновременно, и таким образом уменьшил объем памяти, требуемый для выполнения этого процесса. Однако теперь процесс будет выполняться дольше. Мы решили первую проблему, но предстоит еще разобраться с ошибкой JavaScript heap out of memory.
Копаем глубже в поисках первопричины ошибки “out of memory”
Чтобы лучше понять проблему, первое, что я сделал, — это получил больше информации о профилировании при выполнении теста. Для этого я обновил скрипт test package.json следующим образом:
{
"scripts": {
"test": "node --expose-gc ./node_modules/.bin/jest --runInBand --logHeapUsage",
}
}
Это позволило мне запускать тесты по отдельности и получать информацию о том, сколько памяти выделялось после запуска каждого теста.
В этот момент я понял, что Jest потребляет больше памяти, чем доступно в контейнере Jenkins, и что для запуска первого теста потребовалось много времени.
Моим первым решением было “разогнать” тест с помощью SWC, который должен был ускорить компиляцию в 20 раз по сравнению с Babel. Однако я столкнулся с несколькими проблемами во время настройки, поскольку оказалось, что он неспособен на должную обработку циклических зависимостей.
Затем в процессе поиска в Google другого варианта я нашел опцию конфигурации ts-jest, которая называлась Isolated Modules. Если говорить вкратце, она отключает проверку типов TypeScript, что приводит к меньшему потреблению памяти и меньшим временным затратам.
Окончательное решение
Полагаю, у вас есть опасения по поводу проверки типов, ведь мы не используем TypeScript, чтобы можно было ее отключить вначале. Но не беспокойтесь, мы просто отключим ее в среде CI. Как вы можете видеть в приведенных ниже скриптах, нам нужно отключить проверку типов для CI-скрипта, отправив переменную среды. Затем необходимо только убедиться в том, что вы используете test:ci при настройке конвейера.
pipeline {
agent { docker { image 'node:18.7-alpine' } }
stages {
stage('test') {
steps {
sh 'npm run test:ci'
}
}
}
}
const isolatedModules = process.env.ISOLATED_MODULES === 'true';
module.exports = {
// [...]
globals: {
'ts-jest': {
isolatedModules
}
}
};
{
"scripts": {
"test": "jest",
"test:ci": "ISOLATED_MODULES=true node --expose-gc ./node_modules/.bin/jest --runInBand"
}
}
Ниже прилагаю два скриншота: обычное тестирование и CI-тестирование. Сравните результаты.
Если мы посмотрим на три этапа на изображениях выше и проведем сравнение между каждым из них, то увидим следующее.
- Выполнен первый тест: в CI-скрипте время запуска и потребляемая память составляют на 3065 с и на 170 МБ меньше соответственно.
- Выполнен последний тест: окончательный размер кучи в CI-скрипте меньше на 167 МБ.
- Итог выполнения: процесс выполнения тестирования длился на 11 375 с меньше в CI-скрипте.
Наконец, мы видим, что после оптимизации тесты выполняются более чем на 50% быстрее в среде CI.
Читайте также:
Перевод статьи Carlos Fernando Arboleda Garcés: How To Improve the Jest Performance in CI Environments When Using TypeScript