Вы можете использовать Jest для тестирования инструментов командной строки.

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

Зачем нужны тест-кейсы

Преимущества

1. Обеспечение качества кода и повышение доверия

Глядя на Github в целом, зрелая библиотека инструментов должна иметь:

• Хорошо разработанные тестовые примеры (Jest/Mocha)

• Дружественная документация (официальный сайт/демо)

• Файл объявления типа d.ts

• Среда непрерывной интеграции (git action/circleci)

Без этих элементов пользователи могут столкнуться с различными ошибками при использовании продукта, и ясно, что они, конечно, не хотят, чтобы это происходило, что в конечном итоге затрудняет принятие пользователями вашего продукта.

Наиболее важным моментом тестовых примеров является улучшение качества кода, чтобы у других была уверенность в использовании разрабатываемых вами инструментов (отношения доверия, порождаемые доверием, имеют решающее значение для разработчиков программного обеспечения).

Кроме того, тест-кейсы можно непосредственно рассматривать как готовые среды отладки, которые постепенно восполнят непродуманные кейсы в анализе требований при написании тест-кейсов.

2. Реорганизация гарантии

Хорошо разработанные тестовые примеры могут сыграть критическую роль в рефакторинге, когда код необходимо обновить для основных выпусков.

Выберите метод разработки тестового примера тестирования черного ящика, заботьтесь только о вводе и выводе, не нужно заботиться о том, что делает тестовый пример внутри.

При рефакторинге, если окончательный API-интерфейс, предоставляемый пользователю, не меняется, то практически никаких изменений не требуется, и повторно используются предыдущие тестовые примеры.

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

3. Повышение читабельности кода

Для разработчиков, которые хотят понять исходный код проекта, чтение тестовых случаев является эффективным способом.

Тестовые случаи обеспечивают очень наглядное представление о функциональности и поведении инструмента в различных сценариях.

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

// Vue.js test cases
// https://github.com/vuejs/core/blob/main/packages/reactivity/__tests__/computed.spec.ts#L24
it('should compute lazily', () => {
  const value = reactive<{ foo?: number }>({})
  const getter = jest.fn(() => value.foo)
  const cValue = computed(getter)
  // lazy
  expect(getter).not.toHaveBeenCalled()
expect(cValue.value).toBe(undefined)
  expect(getter).toHaveBeenCalledTimes(1)
  // should not compute again
  cValue.value
  expect(getter).toHaveBeenCalledTimes(1)
  // should not compute until needed
  value.foo = 1
  expect(getter).toHaveBeenCalledTimes(1)
  // now it should compute
  expect(cValue.value).toBe(1)
  expect(getter).toHaveBeenCalledTimes(2)
  // should not compute again
  cValue.value
  expect(getter).toHaveBeenCalledTimes(2)
})

Недостатки

У всего есть две стороны, поэтому после разговора о преимуществах давайте поговорим о недостатках, чтобы помочь вам лучше определиться, стоит ли вам писать тестовые примеры для вашего проекта.

Нет времени

Когда разработчики, как правило, не пишут тестовые примеры, наиболее распространенный аргумент заключается в том, что они слишком громоздки.

Я пишу тестовые случаи времени, код давно написан, вы говорите тестирование? Оставьте это QA, это их обязанность.

Эта ситуация вполне понятна, обычно время разработки слишком поздно, как мы можем уделить время написанию тестовых случаев?

Таким образом, вам нужно судить о ценности написания тестовых случаев в зависимости от типа проекта.

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

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

Напротив, для библиотек инструментов и библиотек компонентов, поскольку функциональных изменений немного и, как правило, отсутствуют ресурсы контроля качества, рекомендуется дополнять тестовые примеры, если уже существует определенная пользовательская шкала.

Не могу написать

Написание тестовых случаев требует изучения синтаксиса тестовой среды, поэтому требует некоторых затрат на обучение (отсутствие времени на изучение также является причиной того, что вы не пишете)

Хорошо то, что основная среда тестирования на рынке более или менее одинакова, общая идея имеет тенденцию быть последовательной, в то время как самих критических изменений не так много. Обычные разработчики могут начать работу за неделю, две недели вперед. После обучения можно использовать в любом front-end проекте (выучил один раз, пиши везде).

По сравнению со стоимостью обучения Vue.js и React и в сочетании с преимуществами предыдущих, это очень хорошая сделка?

После преимуществ и недостатков давайте поделимся опытом написания тестовых случаев для сложного инструмента командной строки.

Интеграционное тестирование инструментов командной строки

Разница между интеграционными тестами и модульными тестами заключается в том, что первые шире, а вторые более детализированы, а интеграционные тесты также можно комбинировать из нескольких модульных тестов.

Поскольку это инструмент командной строки, первое, о чем нужно подумать, это как имитировать поведение пользователя с помощью командной строки.

Запустить командную строку напрямую

Сначала я понимал, что тестовые случаи должны максимально имитировать исходный пользовательский ввод. Поэтому я подумал о том, чтобы запустить инструмент командной строки непосредственно в тестовом примере.

Запустите инструмент командной строки в дочернем процессе, затем распечатайте выходные данные дочернего процесса в родительском процессе и, наконец, определите, соответствует ли результат печати ожидаемому.

Преимущество: больше соответствует тому, как пользователи используют командную строку.

Недостаток: когда вам нужно отладить тестовые случаи, отладчик начинает работу с очень низкой производительностью из-за зависимости от подпроцессов, а тестовые случаи часто истекают по времени или даже проглатывают ошибки или выводят некоторые системные ошибки, которые не связаны с самими тестовыми примерами.

Функция «Выполнить»

Последнее решение слишком сильно отставало и было вынуждено думать о других решениях.

Поскольку я использую commander для реализации инструмента командной строки для Node.js, тестовые примеры, по сути, просто должны получить действие, стоящее за командой для выполнения.

В документации Commander упоминается, что вызов метода parse с аргументами командной строки вызовет обратный вызов действия.

Итак, мы предоставляем функцию запуска начальной загрузки, которая принимает аргументы командной строки и передает их в синтаксический анализ.

Преимущества: отсутствие зависимости от дочерних процессов, запуск тестов непосредственно в текущем процессе, отладчик также не вызывает проблем, успешно устраняет узкое место в производительности.

Недостатки: в коде есть побочные эффекты, все тестовые наборы используют один и тот же экземпляр программы, тестовые наборы можно без проблем использовать по отдельности, но несколько тестовых наборов могут мешать друг другу.

Заводская функция

Научившись в прошлый раз, мы открываем фабричную функцию, которая напрямую генерирует инструменты командной строки.

Таким образом, каждый раз, когда вы запускаете тест-кейс, вы создаете отдельную программу, делая тест-кейсы изолированными друг от друга.

После рассмотрения инициализации инструмента командной строки давайте рассмотрим несколько специальных тестов для случая командной строки.

Команда справки по тестированию

При тестировании команд справки (—help, -h) или тестировании проверки аргументов командной строки, command выводит текст приглашения в процесс в виде журнала ошибок и вызывает process.exit для выхода из текущего процесса.

Это приведет к досрочному завершению тестового случая, поэтому такое поведение необходимо переписать.

Внутренне Commander предоставляет переопределенную функцию exitOverride, которая выдает ошибку JavaScript вместо выхода исходного процесса.

После переопределения поведения при выходе для проверки текста команды справки вам также необходимо использовать configureOutput, предоставленный командующим.

Затем измените тестовый пример:

Тестирование асинхронных вариантов использования

Инструменты командной строки могут иметь асинхронные обратные вызовы, а тестовые случаи должны поддерживать асинхронные случаи.

Хорошо, что Jest готов к работе с асинхронными тестовыми случаями, поэтому в качестве примера возьмем команду help.

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

Jest имеет тайм-аут по умолчанию 5000 мс, который также можно переопределить с помощью файла конфигурации/тестового примера.

В дополнение к тайм-ауту, количество добавленных утверждений также является точкой, обеспечивающей успех асинхронных тестовых случаев.

expect.assertions может указать, сколько раз один тестовый пример будет запускать утверждение, что полезно для тестирования сценариев перехвата исключений.

И тайм-ауты, и ожидаемое количество раз не совпадают, что приведет к сбою тестового примера.

Переменные в тестовых запусках

В обычном тестовом сценарии значение переменной можно проверить, запустив возвращаемое значение экспортированной функции.

Однако инструменты командной строки могут полагаться на контекстную информацию (аргументы, параметры) и плохо подходят для внутреннего дизассемблирования и экспорта отдельных функций, так как же проверить значения переменных во время выполнения?

Я использовал debug + jest.doMock + toHaveBeenCalled

1. Используйте модуль debug для печати параметров, которые необходимо проверить (несколько инвазивно в коде, но модуль отладки также можно использовать для ведения журнала).

2. Среда выполнения тестового примера захватывает модуль отладки с помощью jest.doMock, так что выполнение отладки возвращает jest.fn.

3. Подтвердите ввод jest.fn с помощью toHaveBeenCalled.

Зачем использовать jest.doMock вместо jest.mock?

jest.mock объявляет подъем во время выполнения, что делает невозможным использование внешней переменной f

https://github.com/facebook/jest/issues/2567

Имитация взаимодействия с командной строкой

Инструменты командной строки с взаимодействием с командной строкой — очень распространенный сценарий.

Вдохновленный vue-cli, имитация пользовательского ввода в тестовых примерах становится очень простой и ненавязчивой в любом коде.

1. Создайте __mock__/inquirer.js, перехватите и прокси-модуль подсказки и добавьте оператор утверждения Jest в перереализованную функцию подсказки.

2. Смоделируйте вопросы и ответы пользователя через expectPrompts перед запуском тестового примера (условия для создания утверждений).

3. Когда код запускает inquirer.prompt, прокси-сервер переходит к самоопределяемой подсказке __mock__/inquirer.js, и подсказка будет соответствовать (потреблять данные) в порядке, основанном на вопросах и ответах, созданных предыдущим expectPrompts.

4. Окончательное приглашение прокси-сервера вернет тот же объект ответов, что и реальное приглашение, что сделает окончательное поведение согласованным.

Краткое содержание

Убедитесь, что тестовые наборы написаны независимо друг от друга, не влияют друг на друга, не имеют побочных эффектов и обладают идемпотентностью.

Это можно сделать со следующих точек зрения:

• Каждый раз, когда вы запускаете тестовый пример, создавайте новый экземпляр Commander.

• Разрешить одному тестовому набору использовать одноэлементный режим, не позволять нескольким тестовым наборам использовать один и тот же одноэлементный режим.

• Изоляция файловой системы.

Другие советы по тестированию

Каталог вакансий по моделированию

jest.spyOn(process, ‘cwd’).mockImplementation(() => mockPath))

jest.spyOn отслеживает вызовы process.cwd, а jest.mockImplementation переписывает поведение process.cwd с целью имитации рабочего каталога.

Если вы не полагаетесь на Jest API, вы также можете передать фиктивный рабочий каталог в качестве параметра фабричной функции createProgram.

Моделирование файловых систем

Чтение и запись файлов также являются операциями с побочными эффектами, а поскольку инструменты командной строки могут включать модификации файлов, нет гарантии чистой среды при каждом запуске тестового примера. Чтобы убедиться, что тестовые случаи независимы друг от друга, необходимо смоделировать реальную файловую систему.

Выберите здесь memory-fs, что преобразует операции реальной файловой системы в виртуальные файлы в памяти.

Создайте новую папку __mocks__ в корне проекта.

Добавьте fs.js в папку __mocks__, чтобы экспортировать модуль memfs.

Jest обрабатывает файлы в папке __mocks__ как модули, которые по умолчанию можно эмулировать.

Запустите jest.mock(fs) в тестовом случае, чтобы захватить модуль fs и проксировать его под __mocks__/fs.js.

Проблема с побочным эффектом файловой системы решается путем проксирования fs в memfs.

Тихий журнал ошибок

jest.mockImplementation не передает никаких аргументов и будет молча обрабатывать функцию после макета.

Реализовать функцию отказа от вывода логов ошибок при выполнении тестов, чтобы тестовые случаи выполнялись чище.

Отсутствие вывода журнала ошибок не означает проглатывание ошибки, вы все равно можете использовать try/catch для проверки сценария ошибки.

Журнал ошибок также можно переписать с помощью program.configureOutput, как упоминалось ранее.

Перед использованием.

После использования.

Крючки жизненного цикла тестовых случаев

Jest предоставляет следующие крючки

• перед всем

• перед каждым

• после каждого

• после всего

Крючки жизненного цикла запускаются до/после каждого/всех тестовых случаев, и, добавляя общедоступный код в хуки, вы можете в некоторой степени уменьшить объем кода.

Например, с помощью хука beforeEach код имитируется перед запуском каждого тестового примера, а после его завершения он восстанавливается с помощью jest.retoreAllMock.

Поддержка машинописного текста

Добавление поддержки TypeScript в тестовые примеры позволяет использовать более надежные подсказки типов и позволяет предварительно проверять типы кода перед запуском тестовых случаев.

  1. Добавьте файл объявления типов ts-jest, typescript, @types/jest.
npm i ts-jest typescript @types/jest -D

2. Добавьте файл tsconfig.json и добавьте ранее установленный @types/jest в список файлов объявлений.

{
 “compilerOptions”: {
   “types”: [ “jest” ],
 }
}

3. Измените суффикс имени файла тестового примера index.spec.js → index.spec.ts и измените введение CommonJS в ESM.

Тестовое покрытие

Визуальное представление запущенных тестовых случаев, невыполненного кода, количества строк.

Добавьте параметр coverage в конец тестовой команды.

jest --coverage

Папка, которая создает покрытие после запуска, содержит отчет о покрытии тестами.

Кроме того, тестовое покрытие может быть интегрировано с платформой CI/CD для создания отчета о тестовом покрытии и загрузки в CDN после каждого выпуска инструмента.

Добейтесь увеличения охвата тестами в каждом выпуске инструмента.

Краткое содержание

Написание тестовых случаев — это способ потратить больше времени заранее (изучение синтаксиса тестового примера) и получить гораздо больше позже (непрерывный контроль качества кода и повышение уверенности в рефакторинге).

Подходит для продуктов с меньшим количеством изменений и меньшим количеством ресурсов контроля качества, таких как инструменты командной строки, библиотеки инструментов.

Совет по написанию тестовых случаев — обратиться к тестовым примерам на Github соответствующего инструмента, часто официальные тестовые примеры более полны.

Интеграционное тестирование инструментов командной строки требует, чтобы тестовые примеры были изолированы друг от друга, чтобы обеспечить идемпотентность.

Предоставьте фабричную функцию, которая создает экземпляр Commander, создавая совершенно новый экземпляр каждый раз при запуске тестового примера.

Используйте встроенный API Jest, например jest.spyOn , mockImplementation, jest.doMock, для прокси-модулей npm или встроенных функций, что менее агрессивно по отношению к коду.

Рекомендации

Блог.

Как лучше всего провести модульное тестирование командного интерфейса?

https://itnext.io/testing-with-jest-in-typescript-cc1cd0095421

Переполнение стека.

https://stackoverflow.com/questions/58096872/react-jest-test-fails-to-run-with-ts-jest-unexpected-token-on-imported-file

Гитхаб.

https://github.com/shadowspawn/forest-arborist/blob/fca5ffcc5b300660ae9e1f6c4a8667d72feb0822/src/command.ts#L48

https://github.com/tj/commander.js/blob/master/tests/command.action.test.js

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter и LinkedIn. Присоединяйтесь к нашему сообществу Discord.