Каждый разработчик и команда разработчиков должны принимать решения при создании нового проекта. Сегодня обсудим проекты Node.js. Если говорить о разработке на JavaScript, то одним из лучших решений, которое вы можете принять, является выбор TypeScript. Этот язык дает дополнительные инструменты для написания более понятного и поддерживаемого кода.

Источники: iconfinder.com, freeicons.io

Еще одно удачное решение  —  добавить в проект тестовый фреймворк или библиотеку. Сейчас наиболее часто используемым фреймворком является 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).
Ошибка JavaScript heap out of memory error

Кроме того, важно отметить, что до обновления зависимостей этап тестирования занимал 1,59 минуты для модульных тестов и 3,26 минуты для e2e-тестов. Запомните эти показатели  —  мы вернемся к ним чуть позже.

Время работы конвейера CI на этапах тестирования

Решение проблемы застревания во время выполнения тестовых примеров

Как видно из ответов на этот вопрос на 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 — опция 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-тестирование. Сравните результаты. 

Без оптимизации — обычный скрипт теста (npm run test)
С оптимизацией — скрипт CI-теста (npm run test:ci)

Если мы посмотрим на три этапа на изображениях выше и проведем сравнение между каждым из них, то увидим следующее.

  • Выполнен первый тест: в CI-скрипте время запуска и потребляемая память составляют на 3065 с и на 170 МБ меньше соответственно.
  • Выполнен последний тест: окончательный размер кучи в CI-скрипте меньше на 167 МБ.
  • Итог выполнения: процесс выполнения тестирования длился на 11 375 с меньше в CI-скрипте.

Наконец, мы видим, что после оптимизации тесты выполняются более чем на 50% быстрее в среде CI.

Время конвейера CI для этапов тестирования после оптимизации

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Carlos Fernando Arboleda Garcés: How To Improve the Jest Performance in CI Environments When Using TypeScript

Предыдущая статьяКак ускорить full-stack разработку, не создавая API
Следующая статья10 языков программирования, которые пригодятся в 2023 году