Разделы портала

Онлайн-тренинги

.
Создание хороших цепочек команд в Cypress
13.07.2022 00:00

Автор: Филип Рик (Filip Hric)
Оригинал статьи
Перевод: Ольга Алифанова

Если вы используете 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') // дочерняя()

Есть команды, меняющие поведение в зависимости от места в цепочке. Например, у нас есть такая 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') // выбирает яблоки в первом списке
cy.get('.second')
     .contains('Apples') // выбирает яблоки во втором списке

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

В Cypress есть встроенные повторные попытки для множества команд. Допустим, у нас есть список элементов, требующих 3 секунд для загрузки. Команда ниже валидна и будет успешно выполнена.

cy.get('li')

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

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

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

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

Распространенный фокус

Однако при создании более длинной цепи команд все становится запутаннее. Наша команда .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() запустится только тогда, когда все четыре <li> элемента присутствуют на странице – не раньше.

Создание хороших цепочек команд

Зная все это, можно эффективнее писать цепочки команд. Так как мы знаем, что

  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()

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

Обсудить в форуме