1000-футовый обзор написания тестов Cypress #frontend@twiliosendgrid

Опубликовано: 2020-10-03

В Twilio SendGrid мы написали сотни сквозных (E2E) тестов Cypress и продолжаем писать новые функции по мере выпуска новых функций в различных веб-приложениях и командах. Эти тесты охватывают весь стек, проверяя, что наиболее распространенные варианты использования, с которыми может столкнуться клиент, по-прежнему работают после внесения новых изменений кода в наши приложения.

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

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

Контур:

  1. Обзор Cypress API
  2. Взаимодействие с элементами
  3. Утверждение элементов
  4. Работа с API и сервисами
  5. Выполнение HTTP-запросов с помощью cy.request(…)
  6. Создание многоразовых плагинов с помощью cy.task()
  7. Имитация сетевых запросов с помощью cy.server() и cy.route()
  8. Пользовательские команды
  9. Об объектах страницы
  10. Выбор не запускать клиентский код с проверками window.Cypress
  11. Работа с фреймами
  12. Стандартизация тестовых сред

Обзор API Cypress

Давайте начнем с тех частей, которые мы чаще всего использовали с Cypress API.

Выбор элементов

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

  • Получение элементов на основе некоторого селектора CSS с помощью cy.get(“[data-hook='someSelector']”) или cy.find(“.selector”) .
  • Выбор элементов на основе некоторого текста, такого как cy.contains(“someText”) , или получение элемента с определенным селектором, который содержит некоторый текст, такой как cy.contains(“.selector”, “someText”) .
  • Заставить родительский элемент выглядеть «внутри», поэтому все ваши будущие запросы будут ограничены дочерними элементами родителя, такими как cy.get(“.selector”).within(() => { cy.get(“.child”) }) .
  • Поиск списка элементов и просмотр «каждого» для выполнения дополнительных запросов и утверждений, таких как cy.get(“tr”).each(($tableRow) => { cy.wrap($tableRow).find('td').eq(1).should(“contain”, “someText” }) .
  • Иногда элементы могут быть вне поля зрения страницы, поэтому вам нужно сначала прокрутить элемент, чтобы он отображался, например, cy.get(“.buttonFarBelow”).scrollIntoView() .
  • Иногда вам потребуется более длительный тайм-аут, чем тайм-аут команды по умолчанию, поэтому вы можете дополнительно добавить { timeout: timeoutInMs } , например cy.get(“.someElement”, { timeout: 10000 }) .

Взаимодействие с элементами

Это наиболее часто используемые взаимодействия, обнаруженные в наших тестах Cypress. Иногда вам нужно добавить свойство { force: true } в эти вызовы функций, чтобы обойти некоторые проверки с элементами. Это часто происходит, когда элемент каким-то образом покрыт или получен из внешней библиотеки, над которой у вас нет большого контроля с точки зрения того, как она отображает элементы.

  • Нам нужно нажимать на многие вещи, такие как кнопки в модальных окнах, таблицы и тому подобное, поэтому мы делаем такие вещи, как cy.get(“.button”).click() .
  • Формы повсюду в наших веб-приложениях для заполнения сведений о пользователе и других полей данных. Мы вводим эти входные данные с помощью cy.get(“input”).type(“somekeyboardtyping”) , и нам может понадобиться очистить некоторые значения по умолчанию для входных данных, сначала очистив их, как cy.get(“input”).clear().type(“somenewinput”) . Есть также классные способы ввода других клавиш, таких как {enter} для клавиши Enter, когда вы делаете cy.get(“input”).type(“text{enter}”) .
  • Мы можем взаимодействовать с опциями выбора, такими как cy.get(“select”).select(“value”) и флажками, такими как cy.get(“.checkbox”).check() .

Утверждение элементов

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

  • Чтобы проверить, отображаются ли вещи на странице, вы можете переключаться между cy.get(“.selector”).should(“be.visible”) и cy.get(“.selector”).should(“not.be.visible”) .
  • Чтобы определить, существуют ли элементы DOM где-то в разметке, и если вам не обязательно заботиться о том, видны ли элементы, вы можете использовать cy.get(“.element”).should(“exist”) или cy.get(“.element”).should(“not.exist”) .
  • Чтобы увидеть, содержит ли элемент какой-либо текст или нет, вы можете выбрать между cy.get(“button”).should(“contain”, “someText”) и cy.get(“button”).should(“not.contain”, “someText”) .
  • Чтобы убедиться, что вход или кнопка отключены или включены, вы можете утверждать так: cy.get(“button”).should(“be.disabled”) .
  • Чтобы утверждать, что что-то проверено, вы можете проверить, например, cy.get(“.checkbox”).should(“be.checked”) .
  • Обычно вы можете полагаться на более осязаемые проверки текста и видимости, но иногда вам приходится полагаться на проверки класса, такие как cy.get(“element”).should(“have.class”, “class-name”) . Существуют и другие подобные способы проверки атрибутов с помощью .should(“have.attr”, “attribute”) .
  • Вам также часто бывает полезно объединять утверждения в цепочку, например, cy.get(“div”).should(“be.visible”).and(“contain”, “text”) .

Работа с API и сервисами

При работе с вашими собственными API и службами, связанными с электронной почтой, вы можете использовать cy.request(...) для выполнения HTTP-запросов к вашим внутренним конечным точкам с заголовками аутентификации. Другой альтернативой является создание cy.task(...) , которые можно вызывать из любого файла спецификаций, чтобы охватить другие функции, которые лучше всего могут выполняться на сервере Node с другими библиотеками, такими как подключение к почтовому ящику и поиск сопоставление электронной почты или больший контроль над ответами и опросом определенных вызовов API, прежде чем возвращать некоторые значения для использования в тестах.

Выполнение HTTP-запросов с помощью cy.request(…)

Вы можете использовать cy.request() для отправки HTTP-запросов к вашему серверному API для настройки или удаления данных перед запуском ваших тестовых примеров. Обычно вы передаете URL-адрес конечной точки, метод HTTP, такой как «GET» или «POST», заголовки, а иногда и тело запроса для отправки на серверный API. Затем вы можете связать это с .then((response) => { }) , чтобы получить доступ к сетевому ответу через такие свойства, как «статус» и «тело». Пример вызова cy.request() здесь.

Иногда вам может быть все равно, произойдет ли сбой cy.request(...) с кодом состояния 4xx или 5xx во время очистки перед запуском теста. Один из сценариев, в котором вы можете игнорировать код состояния сбоя, — это когда ваш тест отправляет запрос GET, чтобы проверить, существует ли еще элемент и был ли он уже удален. Элемент может быть уже очищен, и запрос GET завершится ошибкой с кодом состояния 404 не найден. В этом случае вы должны установить другую опцию failOnStatusCode: false , чтобы ваши тесты Cypress не терпели неудачу даже до запуска тестовых шагов.

Создание многоразовых плагинов с помощью cy.task()

Когда мы хотим иметь больше гибкости и контроля над повторно используемой функцией, чтобы общаться с другой службой, такой как поставщик входящих сообщений электронной почты, через сервер Node (мы рассмотрим этот пример в следующем сообщении в блоге), нам нравится предоставлять свои собственные дополнительные функции и настраиваемые ответы на вызовы API, которые мы связываем и применяем в наших тестах Cypress. Или нам нравится запускать какой-то другой код на сервере Node — мы часто создаем для него плагин cy.task() . Мы создаем функции плагинов в файлах модулей и импортируем их в plugins/index.ts , где мы определяем плагины задач с аргументами, необходимыми для запуска функций, как показано ниже.

Эти плагины можно вызывать с помощью cy.task(“pluginName”, { ...args }) в любом месте ваших спецификационных файлов, и вы можете ожидать, что произойдет та же функциональность. Принимая во внимание, что если вы использовали cy.request() , у вас меньше возможностей для повторного использования, если вы сами не обернули эти вызовы в объекты страницы или вспомогательные файлы для импорта повсюду.

Еще одно предостережение заключается в том, что, поскольку код задачи плагина предназначен для запуска на сервере Node, вы не можете вызывать обычные команды Cypress внутри этих функций, таких как Cypress.env(“apiHost”) или cy.getCookie('auth_token') . Вы передаете такие вещи, как строка токена аутентификации или серверный хост API, в объект аргумента функции вашего плагина в дополнение к вещам, необходимым для тела запроса, если ему нужно общаться с вашим внутренним API.

Имитация сетевых запросов с помощью cy.server() и cy.route()

Для тестов Cypress, требующих данных, которые трудно воспроизвести (например, вариации важных состояний пользовательского интерфейса на странице или работа с более медленными вызовами API), одной из функций Cypress, которую следует учитывать, является заглушение сетевых запросов. Это хорошо работает с запросами на основе XmlHttpRequest (XHR), если вы используете vanilla XMLHttpRequest, библиотеку axios или jQuery AJAX. Затем вы должны использовать cy.server() и cy.route() для прослушивания маршрутов, чтобы имитировать ответы для любого состояния, которое вы хотите. Вот пример:

Другой вариант использования — совместное использование cy.server() , cy.route() и cy.wait() для прослушивания и ожидания завершения сетевых запросов перед выполнением следующих шагов. Обычно после загрузки страницы или выполнения какого-либо действия на странице интуитивно понятный визуальный сигнал сигнализирует о том, что что-то завершено или готово для утверждения и действия. В случаях, когда у вас нет такой видимой подсказки, вы можете явно дождаться завершения вызова API следующим образом.

Одна большая проблема заключается в том, что если вы используете выборку для сетевых запросов, вы не сможете имитировать сетевые запросы или ждать их завершения таким же образом. Вам понадобится обходной путь: замена обычного window.fetch полифиллом XHR и выполнение некоторых шагов по настройке и очистке перед запуском тестов, как указано в этих проблемах . Существует также experimentalFetchPolyfill свойство FetchPolyfill, начиная с Cypress 4.9.0, которое может работать для вас, но в целом мы все еще ищем лучшие методы для обработки сетевых заглушек при выборке и использовании XHR в наших приложениях без каких-либо нарушений. Начиная с Cypress 5.1.0, есть многообещающая новая cy.route2() (см. документацию Cypress ) для экспериментальной сетевой заглушки как запросов XHR, так и запросов на выборку, поэтому мы планируем обновить нашу версию Cypress и поэкспериментировать с ней, чтобы увидеть, это решает наши проблемы.

Пользовательские команды

Подобно библиотекам, таким как WebdriverIO, вы можете создавать глобальные настраиваемые команды, которые можно повторно использовать и объединять в цепочки в ваших файлах спецификаций, например, пользовательскую команду для обработки входов в систему через API перед запуском ваших тестовых случаев. После того, как вы разработали их в файле, таком как support/commands.ts , вы можете получить доступ к таким функциям, как cy.customCommand() или cy.login() . Написание пользовательской команды для входа в систему выглядит следующим образом.

Об объектах страницы

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

Вы можете определить базовый класс «Страница» с общей функциональностью, такой как open() , для унаследованных классов страниц, чтобы делиться ими и расширять их. Производные классы страниц определяют свои собственные функции-получатели для селекторов и других вспомогательных функций, при этом повторно используя функциональность базовых классов посредством вызовов типа super.open() , как показано здесь.

Выбор не запускать клиентский код с проверками window.Cypress

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

Во время тестовых прогонов Cypress в браузер будет добавлено свойство window.Cypress . В своем коде на стороне клиента вы можете проверить, нет ли свойства Cypress в объекте окна, а затем выполнить загрузку как обычно. Но если он запускается в тесте Cypress, на самом деле не загружайте файл. Мы также воспользовались проверкой свойства window.Cypress для наших экспериментов A/B, запущенных в нашем веб-приложении. Мы не хотели добавлять больше ненадежности и недетерминированного поведения из экспериментов A/B, потенциально демонстрирующих разные впечатления для наших тестовых пользователей, поэтому мы сначала проверили, что свойство отсутствует, прежде чем запускать логику эксперимента, как показано ниже.

Работа с фреймами

Работа с iframe в Cypress может быть затруднена, поскольку встроенной поддержки iframe нет. Существует работающая [проблема] ( https://github.com/cypress-io/cypress/issues/136 ), заполненная обходными путями для обработки одиночных и вложенных фреймов, которые могут работать или не работать в зависимости от вашей текущей версии Cypress. или iframe, с которым вы собираетесь взаимодействовать. Для нашего варианта использования нам нужен был способ работы с биллинговыми фреймами Zuora в нашей промежуточной среде для проверки потоков обновления API электронной почты и API маркетинговых кампаний. Наши тесты включают в себя заполнение образца платежной информации перед завершением обновления до нового предложения в нашем приложении.

Мы создали пользовательскую команду cy.iframe(iframeSelector) для инкапсуляции работы с iframe. Передача селектора в iframe затем проверяет содержимое тела iframe до тех пор, пока оно не перестанет быть пустым, а затем возвращает содержимое тела, чтобы оно было связано с дополнительными командами Cypress, как показано ниже:

При работе с TypeScript вы можете ввести собственную команду iframe в свой файл index.d.ts :

Чтобы выполнить платежную часть наших тестов, мы использовали пользовательскую команду iframe, чтобы получить содержимое тела iframe Zuora, а затем выбрали элементы внутри iframe и напрямую изменили их значения. Раньше у нас были проблемы с использованием cy.find(...).type(...) и других альтернатив, которые не работали, но, к счастью, мы нашли обходной путь, изменив значения входов и выборок непосредственно с помощью команды вызова, т.е. cy.get(selector).invoke('val', 'some value') . Вам также понадобится ”chromeWebSecurity”: false в файле конфигурации cypress.json , чтобы вы могли обойти любые ошибки перекрестного происхождения. Ниже приведен пример фрагмента его использования с селекторами-заполнителями:

Стандартизация тестовых сред

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

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

Чтобы запустить тесты в различных интерфейсных средах :

Вам нужно будет изменить значение «baseUrl» , доступ к которому осуществляется через Cypress.config(“baseUrl”) , чтобы оно соответствовало этим URL-адресам, таким как https://staging.app.com или https://testing.app.com . Это изменяет базовый URL-адрес для всех ваших cy.visit(...) , чтобы добавить их пути. Есть несколько способов установить это, например, установить CYPRESS_BASE_URL=<frontend_url> перед запуском команды Cypress или установить --config baseUrl=<frontend_url> .

Чтобы запустить тесты в разных бэкэнд-средах :

Вам нужно знать имя хоста API, такое как https://staging.api.com или https://testing.api.com , чтобы установить его в переменной среды, такой как «apiHost», и получить доступ с помощью таких вызовов, как Cypress.env(“apiHost”) . Они будут использоваться для ваших cy.request(...) для выполнения HTTP-запросов по определенным путям, например «<apiHost>/some/endpoint», или передаваться вашим вызовам функции cy.task(...) в качестве другого аргумента. свойство, чтобы знать, какой бэкэнд поразить. Эти аутентифицированные вызовы также должны знать токен авторизации, который вы, скорее всего, храните в localStorage или файле cookie через cy.getCookie(“auth_token”) . Убедитесь, что этот токен авторизации в конечном итоге передается как часть заголовка «Авторизация» или каким-либо другим способом как часть вашего запроса. Существует множество способов установить эти переменные среды, например, непосредственно в файле cypress.json или в параметрах командной строки --env , где вы можете указать их в документации Cypress .

Чтобы подойти к входу в систему для разных пользователей или с использованием разных метаданных:

Теперь, когда вы знаете, как работать с несколькими внешними URL-адресами и внутренними хостами API, как вы обрабатываете вход в систему для разных пользователей? Как вы используете различные метаданные в зависимости от среды, такие как вещи, связанные с доменами, ключами API и другими ресурсами, которые могут быть уникальными в тестовых средах?

Давайте начнем с создания еще одной переменной среды под названием «testEnv» с возможными значениями «testing» и «staging», чтобы вы могли использовать ее как способ указать, какие пользователи среды и метаданные должны применяться в тесте. Используя переменную среды «testEnv», вы можете подойти к этому двумя способами.

Вы можете создать отдельные файлы «staging.json», «testing.json» и другие JSON-файлы среды в папке fixtures и импортировать их для использования на основе значения «testEnv», такого как cy.fixture(`${testEnv}.json`).then(...) . Однако вы не можете правильно напечатать файлы JSON, и гораздо больше места для ошибок в синтаксисе и записи всех свойств, необходимых для теста. Файлы JSON также находятся дальше от тестового кода, поэтому вам придется управлять как минимум двумя файлами при редактировании тестов. Аналогичные проблемы с обслуживанием возникнут, если все данные тестов среды будут установлены в переменных среды непосредственно в вашем cypress.json и их будет слишком много, чтобы управлять множеством тестов.

Альтернативным вариантом является создание объекта тестовой оснастки в файле спецификаций со свойствами, основанными на тестировании или подготовке, чтобы загрузить пользователя этого теста и метаданные для определенной среды. Поскольку это объекты, вы также можете определить лучший универсальный тип TypeScript вокруг объектов тестовых приспособлений для повторного использования всех ваших файлов спецификаций и для определения типов метаданных. Вы должны вызвать Cypress.env(“testEnv”) , чтобы увидеть, с какой тестовой средой вы работаете, и использовать это значение для извлечения тестовой оснастки соответствующей среды из общего объекта тестовой оснастки и использовать эти значения в своем тесте. Общая идея объекта тестовых фикстур резюмируется в приведенном ниже фрагменте кода.

Совместное применение значения конфигурации Cypress «baseUrl», переменной среды бэкэнда «apiHost» и переменной среды «testEnv» позволяет нам иметь тесты Cypress, которые работают в нескольких средах без добавления нескольких условий или отдельных логических потоков, как показано ниже.

Давайте сделаем шаг назад, чтобы увидеть, как вы можете даже создавать свои собственные команды Cypress для запуска через npm. Подобные концепции можно применить к пряже, Makefile и другим сценариям, которые вы можете использовать для своего приложения. Вы можете определить варианты команд «открыть» и «запустить», чтобы согласовать с Cypress «открыть» графический интерфейс и «запустить» в автономном режиме для различных внешних и внутренних сред в вашем package.json . Вы также можете настроить несколько файлов JSON для каждой конфигурации среды, но для простоты вы увидите команды со встроенными параметрами и значениями.

Вы заметите в сценариях package.json , что ваш интерфейс «baseUrl» находится в диапазоне от «http://localhost:9001», когда вы запускаете приложение локально, до URL-адреса развернутого приложения, такого как « https://staging.app. ком ». Вы можете установить внутренние переменные «apiHost» и «testEnv», чтобы облегчить выполнение запросов к конечной точке бэкэнда и загрузку определенного объекта тестовой оснастки. Вы также можете создать специальные команды «cicd», когда вам нужно запускать тесты в контейнере Docker с ключом записи.

Несколько выводов

Когда дело доходит до выбора элементов, взаимодействия с элементами и утверждения об элементах на странице, вы можете продвинуться довольно далеко, написав множество тестов Cypress с небольшим списком команд Cypress, таких как cy.get() , cy.contains() , .click() , .type() , .should('be.visible') .

Также есть способы отправлять HTTP-запросы к серверному API с помощью cy.request() , запускать произвольный код на сервере Node с помощью cy.task() () и заглушать сетевые запросы с помощью cy.server() и cy.route() . Вы даже можете создать свою собственную команду, такую ​​как cy.login() , которая поможет вам войти в систему через API. Все эти вещи помогают сбросить пользователя в правильную начальную точку перед запуском тестов. Поместите эти селекторы и функции в файл, и вы создадите многоразовые объекты страницы для использования в своих спецификациях.

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

Это поможет вам запускать разные наборы пользователей с отдельными ресурсами данных в спецификациях Cypress. Отдельные команды Cypress npm, такие как npm run cypress:open:staging в вашем package.json , загрузят правильные значения переменных среды и запустят тесты для выбранной вами среды.

На этом мы завершаем наш тысячефутовый обзор написания тестов Cypress. Мы надеемся, что это предоставило вам практические примеры и шаблоны для применения и улучшения в ваших собственных тестах Cypress.

Хотите узнать больше о тестах Cypress? Ознакомьтесь со следующими ресурсами:

  • Что следует учитывать при написании E2E-тестов
  • TypeScript — все, что нужно для ваших тестов Cypress
  • Работа с потоками электронной почты в тестах Cypress
  • Идеи по настройке, организации и объединению ваших тестов Cypress
  • Интеграция тестов Cypress с Docker, Buildkite и CICD