<style>.lazy{display:none}</style>Как писать эффективные цепочки команд в Cypress
Цепочки команд в Cypress

Как писать эффективные цепочки команд в Cypress

Перевод статьи «Writing better command chains in Cypress».

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

Содержание

Подпишитесь на наш ТЕЛЕГРАМ КАНАЛ ПО АВТОМАТИЗАЦИИ ТЕСТИРОВАНИЯ

Что собой представляют цепочки команд в Cypress

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

cy.get('#element').click()

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

cy.get('#element') // Новая цепочка
  .click()
  .get('#modal') // Новая цепочка
  .type('text{enter}')

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

Поскольку существуют родительские команды, можно предположить, что есть и дочерние команды. Типичным примером дочерней команды являются .type(), .click() или .should(), для применения которых необходим объект. Этот объект задается предыдущей командой – родительской.

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

cy.get('.button') // Родительская
  .contains('Send') // Родительская для .click(), но дочерняя для .get()
  .click('#modal') // child()

Некоторые команды меняют свое поведение в зависимости от позиции в цепочке. Допустим, у нас есть следующая HTML-структура с двумя списками:

<ul id="first-list">
  <li>Apples</li>
  <li>Pears</li>
</ul>
<ul id="second-list">
  <li>Grapes</li>
  <li>Apples</li>
</ul>

В этом сценарии наша команда .contains() будет вести себя по-разному в зависимости от позиции в цепочке:

cy.contains('Apples') // выбирает Apples в первом списке

cy.get('.second')
  .contains('Apples') // выбирает Apples во втором списке

Повторные попытки в Cypress

Во многих командах Cypress встроена функция повторного выполнения. Например, у нас есть список элементов, загрузка которого займет 3 секунды. Следующая команда полностью корректна и будет выполнена.

cy.get('li')

По умолчанию таймаут установлен на 4000 миллисекунд (это можно изменить в файле cypress.json). Если вы знакомы с Selenium, то, возможно, такой таймаут вам известен как FluentWait. Он встроен в большинство команд Cypress, поэтому в наши тесты не нужно добавлять ничего лишнего.

Допустим, в нашем списке 5 элементов. Они не выводятся сразу, но для появления каждого из них в нашем приложении требуется около 200 миллисекунд.

cy.get('li')
  .should('have.length', 5)

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

Распространенные ошибки

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

cy.get('li')
  .eq(0)
  .should('contain.text', 'Apples')

Хотя в таком написании теста нет ничего плохого, это открывает возможность для некоторой нестабильности. Если все наши элементы <ul> и <li> будут отображены одновременно, то мы в безопасности. Но если они будут загружаться по одному, мы столкнемся с проблемой. Наша команда .get() не выберет нужный нам элемент.

Давайте еще раз посмотрим на структуру HTML:

<ul id="first-list"> // Этот список загружается вторым
  <li>Apples</li> 
  <li>Pears</li> 
</ul>
<ul id="second-list"> // Этот список загружается первым
  <li>Grapes</li> 
  <li>Apples</li> 
</ul>

Наша команда .should() попытается найти текст Apples не в том элементе. Разберемся, что происходит:

  1. Наше приложение открывается и начинает загружать списки.
  2. Команда .get() обязательна для любых элементов <li>, которые она может найти.
  3. Наше приложение будет загружать <ul id="second-list">, пока первый список все еще загружается.
  4. Команда .get() немедленно находит наши элементы <li> во втором списке и передает их команде .eq().
  5. Команда .eq(0) отфильтрует наш первый элемент списка, содержащий текст Grapes.
  6. Команда .should() определит, содержит ли этот элемент текст Apples. Это не так, поэтому предыдущая команда выполняется снова.
  7. Но наша команда .eq() снова выполнит то же самое действие, поскольку ей были переданы элементы из <ul id="second-list">.
  8. Наша команда .get() больше никогда не будет вызвана, поэтому даже когда наш <ul id="first-list"> в конце концов будет отображен, наши команды .eq() и .should() не достигнут его.

Чтобы улучшить ситуацию, мы можем исключить команду .eq(0), чтобы наше утверждение заставляло команду .get() повторять попытку, пока не найдется хотя бы какой-то элемент, содержащий наш текст.

cy.get('li')
  .should('contain.text', 'Apples')

Если этого недостаточно, мы можем сначала убедиться, что количество наших элементов <li> корректно, а затем сделать наше утверждение:

cy.get('li')
  .should('have.length', 4)
  .eq(0)
  .should('contain.text', 'Apples')

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

Как составлять цепочки команд в Cypress более эффективно

Итак, мы знаем, что:

  1. Каждая команда ожидает завершения предыдущей.
  2. Команды передают информацию друг другу.
  3. .should() делает повторную попытку выполнения предыдущей команды.

Мы можем предотвратить нестабильность и сделать наши команды более эффективными. Допустим, у нас есть окно поиска, которое будет отображать наши элементы из API. Написание такого теста может быть ненадежным, потому что когда мы пишем нашу поисковую фразу, запросы API запускаются при каждом нажатии буквы, а рендеринг ответа в приложении занимает пару секунд (это, конечно, зависит от реализации, но такая практика – не редкость).

cy.get('#search').type('Apples')
cy.get('.result').contains('Apples').click()

Посмотрев на этот пример, вы поймете, почему он может быть нестабильным. Наш элемент .result может быть повторно отображен с новым текстом по мере поступления ответов от API. Это может привести к ошибке Element detached from DOM, с которой вы могли столкнуться. Чтобы сделать этот тест более стабильным, можно написать его следующим образом:

cy.get('#search').type('Apples')
cy.contains('.result', 'Apples').click()

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

Надеюсь, эта статья вам поможет в написании более эффективных цепочек команд.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *