В этой статье мы расскажем о 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 результата, а когда закончим ввод, получим только один результат:
- В строке поиска набирается клавиша “f”.
- Отправляется запрос на поиск всех элементов с “f”.
- Полученный ответ приводит к отображению двух результатов в приложении.
- В строке поиска набирается клавиша “o”.
- Отправляется запрос на поиск всех элементов с “fo”.
- Полученный ответ приводит к отображению двух результатов.
- В строке поиска набирается клавиша “r”.
- Отправляется запрос на поиск всех элементов с “for”.
- Cypress завершает ввод и переходит к следующей команде.
- Cypress выбирает элементы [data-cy=result-item] и выделяет первый из них (с использованием .eq(0)).
- Cypress проверяет, содержит ли выбранный элемент текст “search for critical bugs”.
- Поскольку текст отличается, выполняется предыдущая команда (.eq(0)).
- Во время повторного выполнения приходит ответ на последний запрос, и приложение отображает только один элемент.
- Выбранный в шаге 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)».