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

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

.
8 распространенных ошибок в Cypress, и как их избежать
14.09.2023 00:00

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

Эта статья родилась из доклада, который я делал на тест-фестивале Front End, если хотите посмотреть видео – всегда пожалуйста.

Иногда на моем Discord-сервере я сталкиваюсь с распространенным шаблоном, отвечая на вопросы. Определенные группы проблем склонны регулярно всплывать, и именно про них и речь в этой статье. Приступим!

#1: Использование явного ожидания

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

// ❌ неправильный способ, не делайте так
cy.visit('/')
cy.wait(10000)
cy.get('button')
.should('be.visible')

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

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

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

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

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

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

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

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

❌ выбор элементов при помощи xpath
// Выбор элемента по тексту
cy.xpath('//*[text()[contains(.,"My Boards")]]')
// Выбор элемента с определенным дочерним элементом
cy.xpath('//div[contains(@class, "list")][.//div[contains(@class, "card")]]')
// Фильтрация элемента по индексу
cy.xpath('(//div[contains(@class, "board")])[1]')
// Выбор элемента, находящегося после определенного элемента
cy.xpath('//div[contains(@class, "card")][preceding::div[contains(., "milk")]]')

✅ Выбор элементов при помощи команд cypress
// Выбор элемента по тексту
cy.contains('h1', 'My Boards')
// Выбор элемента с определенным дочерним элементом
cy.get('.card').parents('.list')
// Фильтрация элемента по индексу
cy.get('.board').eq(0)
// Выбор элемента, находящегося после определенного элемента
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 заканчивает свою работу, за дело берется следующая команда. Поэтому как только элемент найден командой the .get(), мы переходим к команде the.eq(0)`. После этого мы переходим к ассерту, который провалится.

Возможно, вас удивляет, что Cypress в этот момент не предпринимает повторных попыток – но на самом деле он это делает. Просто повторяется не вся цепочка. По дизайну команда .should() будет пробовать повторить предыдущую команду, но не весь набор. Поэтому жизненно важно лучше проектировать тест, добавляя «охранника». Прежде чем проверять текст на карточке, мы убедимся, что все карточки присутствуют в 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] будут отрисованы после того, как мы получим ответ от конечной точки /api/lists.

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

Cypress не будет ждать запросов, которые приложение выполняет автоматически. Нам нужно явно просить об этом, используя команду intercept:

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

#5: Игнорирование повторной отрисовки DOM

Современные веб-приложения постоянно отправляют запросы для получения информации от базы данных, а затем отрисовывают их в 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». Причина кроется в том, что пока мы еще печатаем, мы сначала получим два результата, а когда закончим – только один. В результате тест будет продвигаться так:

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

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 как можно меньше. Эта стратегия ускорит ваши тесты и даст вам такую же (или даже большую) уверенность в вашем приложении. Допустим, у вас есть навигационная панель со ссылками, которая выглядит как-то так:

<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(page.prop('href'))
})

#8: Повтор одинаковых наборов действий

Мы часто слышим, что код должен следовать принципу DRY – «не повторяйся». Это отличный принцип для кода, но, кажется, он слегка игнорируется в ходе прогона тестов. В примере ниже есть команда cy.login(), которая проходит по шагам авторизации и используется перед каждым тестом:

Cypress.Commands.add('login', () => {
    cy.visit('/login')
    cy.get('[type=email]')
.type('filip+ Этот e-mail адрес защищен от спам-ботов, для его просмотра у Вас должен быть включен Javascript ')
    cy.get('[type=password]')
.type('i<3slovak1a!')
    cy.get('[data-cy="logged-user"]')
.should('be.visible')
})

Абстракция этой последовательности шагов в одну команду – определенно хорошая идея. Это, безусловно, сделает код более «DRY». Однако если мы будем продолжать использовать ее в тесте, выполнение теста будет проходить по этим шагам снова и снова, по сути повторяя одинаковый набор действий.

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

Cypress.Commands.add('login', () => {
    cy.session('login', () => {
        cy.get('[type=email]')
.type('filip+ Этот e-mail адрес защищен от спам-ботов, для его просмотра у Вас должен быть включен Javascript ')
        cy.get('[type=password]')
.type('i<3slovak1a!')
        cy.get('[data-cy="logged-user"]')
.should('be.visible')
    })
})

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

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