<style>.lazy{display:none}</style>8 распространенных ошибок в Cypress

8 распространенных ошибок в Cypress

В этой статье мы расскажем о 8 распространенных ошибках при использовании Cypress и о том, как можно их избежать.

Содержание:

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

1. Использование явного ожидания (explicit waiting)

Всякий раз, когда вы добавляете явное ожидание в свой тест в Cypress, я полагаю, у вас возникает беспокойство по этому поводу. Как быть в тех случаях, когда наши тесты не проходят из-за слишком медленной загрузки страницы? Кажется, что использование cy.wait()– это именно то, что нужно.

// ❌ incorrect way, don’t use

cy.visit('/')
cy.wait(10000)
cy.get('button') 
  .should('be.visible')

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

cy.visit('/')
cy.get('button', { timeout: 10000 })
  .should('be.visible')

Чем такой подход лучше? В этом случае мы будем ожидать появления button не более 10 секунд. Если же кнопка появится раньше, то тест сразу перейдет к следующей команде. Это поможет вам сэкономить время.

2. Использование нечитабельных селекторов

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

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

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

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

// Select an element by text
cy.xpath('//*[text()[contains(.,"My Boards")]]')
// Select an element containing a specific child element
cy.xpath('//div[contains(@class, "list")][.//div[contains(@class, "card")]]')
// Filter an element by index
cy.xpath('(//div[contains(@class, "board")])[1]')
// Select an element after a specific element
cy.xpath('//div[contains(@class, "card")][preceding::div[contains(., "milk")]]')
// Select an element by text
cy.contains('h1', 'My Boards')
// Select an element containing a specific child element
cy.get('.card').parents('.list')
// Filter an element by index
cy.get('.board').eq(0)
// Select an element after a specific element
cy.contains('.card', 'milk').next('.card')

3. Неправильный выбор элементов

Рассмотрим следующую ситуацию: вы хотите выбрать карточку (белый элемент на странице) и проверить ее текст.

Обратите внимание, что оба этих элемента содержат слово “bugs”. Можете ли вы сказать, какую карточку мы получим, исходя из этого кода?

cy,visit('/board/1')
cy.get('[data-cy=card]')
  .eq(0)
  .should('contain.text', 'bugs')

Возможно, вы предполагаете, что это будет первая карточка с текстом “triage found bugs”. Это хороший ответ, однако не самый точный. Правильный ответ – та карточка, которая загрузится первой.

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

Вы можете задаться вопросом, почему Cypress в случае ошибки не делает повторные попытки и не выполняет команды заново. На самом деле, он выполняет, просто не всю цепочку действий. Команда .should() в Cypress повторяет предыдущую команду, но не всю последовательность команд. Поэтому здесь важно создать более надежный дизайн теста и добавить “защиту” – прежде чем проверить текст нашей карточки, убедимся, что все карточки присутствуют в DOM:

cy,visit('/board/1')
cy.get('[data-cy=card]')
  .should('have.length', 2)
  .eq(0)
  .should('contain.text', 'bugs')

4. Игнорирование запросов в приложении

Давайте рассмотрим этот пример кода:

cy.visit('/board/1')
cy.get('[data-cy=list]')
  .should('not.exist')

Когда мы открываем веб-страницу, происходит отправка запросов к серверу. Ответы на эти запросы обрабатываются фронтендом и отображаются на странице. В данном примере элементы с атрибутом [data-cy=list] начинают отображаться после получения ответа от сервера.

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

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

cy.intercept('GET', '/api/lists')
  .as('lists')
cy.visit('/board/1')
cy.wait('@lists')
cy.get('[data-cy=list]')
  .should('not.exist')

5. Игнорирование повторного рендеринга страницы

Современные веб-приложения постоянно отправляют запросы для получения информации из базы данных, а затем отображают её в DOM. В следующем примере мы протестируем строку поиска, где каждое нажатие клавиши генерирует новый запрос. Каждый ответ приводит к повторному отображению контента на нашей странице. В этом тесте мы хотим убедиться, что после ввода слова “for” мы увидим карточку с текстом “search for critical bugs”. Тест выглядит следующим образом:

cy.realPress(['Meta', 'k'])
cy.get('[data-cy=search-input]')
  .type('for')
cy.get('[data-cy=result-item]')
  .eq(0)
  .should('contain.text', 'search for critical bugs')

В этом тесте возникнет ошибка “element detached from DOM”. Это связано с тем, что при вводе текста мы сначала получим 2 результата, а когда закончим ввод, получим только один результат:

  1. В строке поиска набирается клавиша “f”.
  2. Отправляется запрос на поиск всех элементов с “f”.
  3. Полученный ответ приводит к отображению двух результатов в приложении.
  4. В строке поиска набирается клавиша “o”.
  5. Отправляется запрос на поиск всех элементов с “fo”.
  6. Полученный ответ приводит к отображению двух результатов.
  7. В строке поиска набирается клавиша “r”.
  8. Отправляется запрос на поиск всех элементов с “for”.
  9. Cypress завершает ввод и переходит к следующей команде.
  10. Cypress выбирает элементы [data-cy=result-item] и выделяет первый из них (с использованием .eq(0)).
  11. Cypress проверяет, содержит ли выбранный элемент текст “search for critical bugs”.
  12. Поскольку текст отличается, выполняется предыдущая команда (.eq(0)).
  13. Во время повторного выполнения приходит ответ на последний запрос, и приложение отображает только один элемент.
  14. Выбранный в шаге 10 элемент больше не присутствует, и мы получаем ошибку.

Помните, что команда .should() будет повторно выполнять предыдущую команду, но не всю цепочку команд. Это означает, что команда cy.get('[data-cy=result-item]') не вызовется снова. Чтобы исправить эту проблему, мы можем добавить проверку в наш код, чтобы сначала удостовериться, что мы получаем правильное количество результатов, а затем проверить текст результата.

cy.realPress(['Meta', 'k'])
cy.get('[data-cy=search-input]')
  .type('for')
cy.get('[data-cy=result-item]')
  .should('have.length', 1)
  .eq(0)
  .should('contain.text', 'search for critical bugs')

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

cy.realPress(['Meta', 'k'])
cy.get('[data-cy=search-input]')
  .type('for')
cy.get('[data-cy=result-item]')
  .should( items => {
    expect(items[0].to.have.text('search for critical bugs'))      
  })

6. Создание неэффективных цепочек команд

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

Рассмотрим следующую цепочку команд:

cy.get('[data-cy="create-board"]')
  .click()
  .get('[data-cy="new-board-input"]')
  .type('new board{enter}')
  .location('pathname')
  .should('contain', '/board/')  

Проблема с написанием такой цепочки не только в том, что ее трудно читать, но и в том, что она игнорирует логику построения цепочки родительских/дочерних команд. Каждая команда.get() фактически начинает новую цепочку. Это означает, что цепочка .click().get() на самом деле не имеет смысла. Правильное использование цепочек может предотвратить непредсказуемое поведение ваших тестов и сделать их более читабельными:

cy.get('[data-cy="create-board"]') // parent
  .click() // child
cy.get('[data-cy="new-board-input"]') // parent
  .type('new board{enter}') // child
cy.location('pathname') // parent
  .should('contain', '/board/') // child

7. Чрезмерное использование UI

Я считаю, что при написании UI-тестов следует использовать UI как можно меньше. Такая стратегия позволяет ускорить тестирование и обеспечить такую же (или большую) уверенность в работе приложения. Допустим, у вас есть панель навигации со ссылками, которая выглядит следующим образом:

<nav>
  <a href="/blog">Blog</a>
  <a href="/about">About</a>
  <a href="/contact">Contact</a>
</nav>

Цель теста – проверить все ссылки внутри элемента<nav>. Простой способ – использовать команду .click() и затем проверить содержимое страницы.

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

Вместо такой проверки ссылок можно использовать команду .request(), чтобы убедиться, что страница рабочая:

cy.get('a').each( link => {
  cy.request(link.prop('href'))
})

8. Повторение одного и того же набора действий

Очень часто можно услышать, что ваш код должен быть DRY = don’t repeat yourself. Безусловно, это отличный принцип написания кода, однако, он слегка игнорируется во время тестирования. В примере ниже есть команда cy.login(), которая будет выполнять шаги входа в систему и будет использоваться в каждом тесте:

Cypress.Commands.add('login', () => {

  cy.visit('/login')
  
  cy.get('[type=email]')
    .type('filip+example@gmail.com')
  
  cy.get('[type=password]')
      .type('i<3slovak1a!')

  cy.get('[data-cy="logged-user"]')
    .should('be.visible')

})

Поместить все эти шаги в одну команду – безусловно, хорошо. Это сделает наш код более “чистым”. Но если мы продолжим использовать эту команду в тесте, то, по сути, будем выполнять одни и те же шаги снова и снова и повторять один и тот же набор действий.

В Cypress вы можете воспользоваться приемом, который поможет вам решить эту проблему. Набор шагов можно сохранить и затем использовать повторно с помощью команды cy.session(). Для этого необходимо добавить experimentalSessionAndOrigin:true  в вашем файле cypress.config.js. Проще говоря, вы можете упаковать определенный набор шагов в кастомную команду внутри функции .session() :

Cypress.Commands.add('login', () => {

  cy.session('login', () => {

    cy.get('[type=email]')
      .type('filip+example@gmail.com')
    
    cy.get('[type=password]')
      .type('i<3slovak1a!')

    cy.get('[data-cy="logged-user"]')
      .should('be.visible')

  })

})

Это заставит выполнять одну последовательность шагов только один раз в каждом тесте. Если хотите сохранять эти шаги на протяжении всех тестов, используйте плагин cypress-data-session. Кэширование шагов помогает ускорить тесты, особенно если у вас их много.

Перевод статьи «8 common mistakes in Cypress (and how to avoid them)».

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

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