Нюансы тестирования в парочке cucumber.js + puppeteer

The English version is available here

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

Фэйлится степ в сценарии

Этот симптом не связан с таймаутами или кривой реализацией степа. Проблема в другом, ведь у нас есть браузер — это по сути среда, что не подвластна нам, в полной мере, даже с использованием puppeteer - кукловода хрома.

Просто, в какой-то момент времени, планеты в космосе встают в неудобное положение и влияют на браузер хром в вашем докер файле. Увы ничего не поделать, такова жизнь. И ваш степ накрывается медным тазом, а ведь вы так хотели побыстрее смыться домой или похвалить себя за прошедшие тесты или в конце концов, не хотите носить маску BoJack probably horseman на своем лице.

Ну хватит лирики, пора приступить к решению этой проблемы. Для этого в cucumber.js есть необходимые опции для степа.

{ wrapperOptions: { retry: 3 }, timeout: 30000 }

И как вы уже интуитивно поняли:

  • retry — ключ указывающий на кол-во повторов запуска степа +1. Так как учитывается еще и первый запуск, который может провалиться. И если он провалится, то будет повторен 3 раза, прежде чем будет признан недееспособным.
Then('Some step definition', { wrapperOptions: { retry: 4 }, timeout: 30000 }, () => {
return 'pending';
});

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

defineStep(pattern[, options], fn)

  • pattern: Регулярное выражение или строка описывающая Gherkin степ.
  1. timeout - таймаут для степа, который переопределяет таймаут по умолчанию.
  • fn — JS функция объявления степа.

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

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

А что по поводу повторного запуска сценария? К сожалению, в стабильном релизе cucumber.js нет пока такой возможности. В cucumber же, что родом из Ruby, повторный запуск сценария предусмотрен.

Вот тут уже на подходе нормальная реализация --retry опции для запуска сценариев. А здесь закрытая issue с горячими обсуждениями фитчи перезапуска тестов N раз. Судя по документации еще не принятого PR

Use ` — retry <int>` to rerun tests that have been failing. This can be very helpful for flaky tests.
To only retry failing tests in a subset of test use ` — retryTagFilter <EXPRESSION>` (use the same as in Use [Tags](#tags))

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

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

Тестирование вставки текста

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

Сразу скажу, я проверил, трюки с document.executeCommand('copy') и htmlElement.select() не работают. Так же чтобы воспользоваться новой асинхронной Clipboard API, вам понадобятся разрешения, на исполнение операций копирования в буфер или его чтения.

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

Но кроме секьюрити, есть еще и аппаратные моменты.

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

Можно прочитать обо всех нюансах записи/чтения в буфер в статье.

Решить эту проблему можно настройками puppeteer.

const context = scope.browser.defaultBrowserContext();await context.overridePermissions(scope.host, ['clipboard-write', 'clipboard-read']);

Для начала нужно получить текущий контекст браузера при помощи

scope.browser.defaultBrowserContext()

Далее нужно вызвать метод

context.overridePermissions('http://localhost:9001', ['clipboard-write', 'clipboard-read'])

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

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

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

async function copyTextToClipboard(text) {   const page = scope.context.currentPage;   return page.evaluate(textValue => {      return navigator.clipboard.writeText(textValue);   }, text);}

Пропустить некоторые сценарии

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

И так тут есть так сказать координальные отличия от рубяшного cucumber. Если в рубяшном можно указать --tags=~@skip , то в cucumber.js, нужно воспользоваться более понятным для обычного человека выражением --tags="not @skip" . И в целом, команда будет выглядеть следующим образом:

npx cucumber-js --tags="not @skip"

Таким образом, будут выполнены все тесты, кроме тех, что были помечены тегом @skip.

Запустить какой-то один feature файл или сценарий в нем

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

npx cucumber-js --tags=@only
В этом случае будет запущен весь файл feature со всем сценариями в нем
А вот так, будет запущен конкретно один, сценарий в feature файле

Если зафейлился один сценарий, пропустить остальные

Еще одна опция в cucumber.js, которая позволит сэкономить время, в случае с упавшим сценарием — --fail-fast.

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

npx cucumber-js --fail-fast --tags="not @skip"

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

Если один сценарий провалился, все остальные пропускаются и пишется в консоле сколько пропущено.

Используйте методы апи Puppeteer с префиксом wait

В случае, когда нужно подождать появления какого-либо текста или элемента на странице, стоит воспользоваться существующим API в puppeteer. Нужно постараться свести к минимуму использование таймаутов, так как локальное окружение и на CI по разному себя ведет. Разные мощности и ресурсы.

К примеру, если вам нужно проверить наличие текста на странице, то стоит воспользоваться следующим трюком:

async function waitForText(text) {
await scope.context.currentPage.waitForXPath(
`//*[contains(normalize-space(string(.)), '${text}')]`);
}

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

Если же, по аналогии, нужно дождаться, когда определенный элемент будет скрыт или невидим, то можно написать следующий экшн:

async function waitForElementHides(elementType, elementName) {
const selector = scope.context.currentSelectors[elementType][elementName];
await scope.context.currentPage.waitForXPath(selector, { hidden: true });
}

Используется опция { hidden: true }

Или можно подождать пока не появится элемент с определенным CSS селектором:

await scope.context.currentPage.waitForSelector(selector, { visible: true });

Указываем, что нам нужно подождать именно видимого элемента, о чем говорит опция { visible: true }.

Пишите степы, которые могут дать больше информации при фейле

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

К примеру, вот степ для клика на элемент в DOM:

Информативный степ для клика по DOM элементу по его селектору

В этом случае, вы сначала убеждаетесь в наличии элемента в DOM с определенным контентом, при помощи кастомного метода findElement. И только после этого, производится клик на элементе. Все из-за того, что сложно составить такой css или xpath селектор, который бы удовлетворил необходимым требованиям.

Вот реализация метода findElement

Передавайте regex в хендлеры методов API puppeteer как текст

Есть одна проблемка, при сериализации RegExp объекта, что передается в параметры любого из методов API puppeteer, который позволяет выполнять код в контексте браузера. Вот тут закрытая issue по этому вопросу. Все дело в сериализации объектов, для передачи данных между браузерным и puppeteer.

Для передачи RegEx можно воспользоваться массивом

В примере выше, можно заметить что массив более удобен, чем два разных параметра. Таким образом, мы как бы разбираем RegExp на две составляющие source и flags. А затем, собираем вновь.

Проблема с кодом 160 символа

Так как для сохранения пробелов внутри некоторых HTML элементов, используется &nbsp; HTML-entity и <wbr> тег, то появляется символ 160, который увы не поддается RegEx /\s+/gm. Странный момент я обнаружил при работе с контентом внутри контекста браузера, если производится работа с текстовым контентом, через RegExp в хендлере API метода Puppeteer — символ с кодом 160, не обрабатывается и не заменяется на пробел, в случае с replace. Хотя в объявлении для \s+ код символа присутствует как \u00a0 (юникод кодировка) или \x0A в 16-ричном формате.

Если воспользоваться консолей браузера, то символ очищается без проблем при помощи \s+, но для puppeteer нужно немного другое решение — использование символа 160 в RegEx объявлении, внутри метода.

.replace(new RegExp(String.fromCharCode(160), 'g'), ' ')

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

Заключение

Надеюсь, что представленные в этой статье советы из практики, которые я почерпнул из своего OSS проекта, будут полезны и вам!

Спасибо большое за прочтение!

Mad Devs — блог об IT

Engineering your growth. Mad Devs is the team behind large scalable projects, globally.

Mad Devs — блог об IT

Mad Devs is a Cambridge-headquartered IT company developing enterprise-level software solutions for finance, transportation & logistics, security, edtech, and advertising industries. For more information about us, please browse our website: https://maddevs.io/

Alexander Vishnyakov

Written by

«Переписывание с нуля гарантирует лишь одно — ноль!» — Мартин Фаулер

Mad Devs — блог об IT

Mad Devs is a Cambridge-headquartered IT company developing enterprise-level software solutions for finance, transportation & logistics, security, edtech, and advertising industries. For more information about us, please browse our website: https://maddevs.io/

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store